欢迎光临
我们一直在努力

图论专题(二十二):并查集的“逻辑审判”——判断「等式方程的可满足性」

哈喽各位,我是前端小L。

欢迎来到我们的图论专题第二十二篇!并查集(Union-Find)最擅长维护的是什么?是**“集合”。而在逻辑学中,“相等”关系(Equality)天然就具有传递性**(若 a==b 且 b==c,则 a==c),这正好对应了并查集中的连通性。

今天,我们要面对一组混杂着 == 和 != 的方程组。

  • == 代表“连接”(Union)。

  • != 代表“冲突检查”(Check)。

我们的策略非常清晰:先把所有宣称是“朋友”的人聚在一起,然后再检查那些宣称是“敌人”的人,有没有混在同一个圈子里。

力扣 990. 等式方程的可满足性

https://leetcode.cn/problems/satisfiability-of-equality-equations/

题目分析:

  • 输入:一个字符串数组 equations。每个字符串形如 "a==b" 或 "a!=b"。变量名只有小写字母 'a' 到 'z'。

  • 目标:判断这组方程是否逻辑自洽。如果存在矛盾(例如 a==b, b==c 但 a!=c),返回 false;否则返回 true。

核心洞察:处理顺序是关键 如果我们一边处理 == 一边处理 !=,可能会出问题。 比如:a!=b, b==c, a==c。

  • 看 a!=b:目前 a 和 b 不连通,没毛病。

  • 看 b==c:连通 b 和 c。

  • 看 a==c:连通 a 和 c。 这就漏掉了矛盾!因为 a 和 c 连通后,通过 c 这个中介,a 和 b 其实也连通了,这就违背了第一条 a!=b。

  • 正确的策略:

  • 先礼:无视所有 !=,先把所有 == 的关系处理完,构建好所有的“等价集合”(连通分量)。

  • 后兵:遍历所有 != 的关系,检查每一对变量。如果它们在并查集中竟然拥有同一个老大(即它们是等价的),那就自相矛盾了!

  • 算法流程:两遍扫描

  • 初始化并查集:

    • 由于变量只是 'a'-'z',我们只需要一个大小为 26 的 parent 数组。parent[i] = i。

  • 第一遍扫描 (==):

    • 遍历所有方程。如果中间符号是 ==(eq[1] == '='):

      • 提取变量 u = eq[0] – 'a', v = eq[3] – 'a'。

      • 执行 union(u, v)。

  • 第二遍扫描 (!=):

    • 再次遍历所有方程。如果中间符号是 !=(eq[1] == '!'):

      • 提取变量 u, v。

      • 审判时刻:检查 find(u) 是否等于 find(v)。

      • 如果相等,说明 u 和 v 在之前的等式逻辑中已经被判定为“相等”了,现在你又说它俩不等,这就是逻辑矛盾!直接返回 false。

  • 通过:如果所有 != 检查都平安无事,返回 true。

  • 代码实现 (轻量级并查集)

    因为只需要处理26个字母,我们可以直接把并查集的逻辑写在主函数里,或者写一个极简的内部类。

    C++

    #include <vector>
    #include <string>
    #include <numeric>

    using namespace std;

    class Solution {
    private:
    // 简单的并查集实现
    vector<int> parent;

    int find(int x) {
    if (parent[x] != x) {
    parent[x] = find(parent[x]); // 路径压缩
    }
    return parent[x];
    }

    void unite(int x, int y) {
    int rootX = find(x);
    int rootY = find(y);
    if (rootX != rootY) {
    parent[rootX] = rootY;
    }
    }

    public:
    bool equationsPossible(vector<string>& equations) {
    // 1. 初始化并查集,容量 26
    parent.resize(26);
    iota(parent.begin(), parent.end(), 0); // 0, 1, …, 25

    // 2. 第一遍扫描:处理所有 "=="
    for (const string& eq : equations) {
    if (eq[1] == '=') {
    int u = eq[0] – 'a';
    int v = eq[3] – 'a';
    unite(u, v);
    }
    }

    // 3. 第二遍扫描:检查所有 "!="
    for (const string& eq : equations) {
    if (eq[1] == '!') {
    int u = eq[0] – 'a';
    int v = eq[3] – 'a';
    // 如果 u 和 v 属于同一个集合(相等),但等式要求不等 -> 矛盾
    if (find(u) == find(v)) {
    return false;
    }
    }
    }

    // 4. 没有矛盾
    return true;
    }
    };

    深度复杂度分析

    • N:方程的数量。

    • 时间复杂度 O(N):

      • 我们遍历了方程数组两次。

      • 每次并查集操作(find/unite)在路径压缩下接近 O(1)(准确说是阿克曼反函数,对于26个节点简直快到飞起)。

      • 所以总时间是线性的。

    • 空间复杂度 O(1):

      • parent 数组的大小固定为 26,是常数级空间。

    总结:并查集——处理等价关系的专家

    今天这道题,展示了并查集在逻辑推理中的应用。它告诉我们:

    凡是涉及“分组”、“归类”、“等价传递”的问题,都可以抽象为并查集的模型。

    • Union = 建立等价关系。

    • Find = 验证等价关系。

    通过巧妙的两遍扫描(先建关系,后查冲突),我们轻松破解了这个逻辑谜题。

    在下一篇中,我们将挑战一个更加复杂、更贴近实际业务的并查集应用——“账户合并”。当一个用户拥有多个邮箱,而不同的邮箱列表又存在交集时,我们该如何理清这些混乱的账户关系?

    下期见!

    赞(0)
    未经允许不得转载:171主机测评 » 图论专题(二十二):并查集的“逻辑审判”——判断「等式方程的可满足性」
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址