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

哈喽各位,我是前端小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==ca!=c),返回 false;否则返回 true

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

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

  2. b==c:连通 b 和 c。

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

正确的策略

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

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

算法流程:两遍扫描

  1. 初始化并查集

    • 由于变量只是 'a'-'z',我们只需要一个大小为 26 的 parent 数组。parent[i] = i
  2. 第一遍扫描 (==)

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

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

      • 执行 union(u, v)

  3. 第二遍扫描 (!=)

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

      • 提取变量 u, v

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

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

  4. 通过 :如果所有 != 检查都平安无事,返回 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 = 验证等价关系。

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

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

下期见!

相关推荐
铁手飞鹰2 小时前
二叉树(C语言,手撕)
c语言·数据结构·算法·二叉树·深度优先·广度优先
专业抄代码选手3 小时前
【Leetcode】1930. 长度为 3 的不同回文子序列
javascript·算法·面试
平凡灵感码头3 小时前
经典按键扫描程序算法实现方式
单片机·矩阵·计算机外设
[J] 一坚4 小时前
深入浅出理解冒泡、插入排序和归并、快速排序递归调用过程
c语言·数据结构·算法·排序算法
czlczl200209254 小时前
算法:二叉搜索树的最近公共祖先
算法
司铭鸿4 小时前
祖先关系的数学重构:从家谱到算法的思维跃迁
开发语言·数据结构·人工智能·算法·重构·c#·哈希算法
SoleMotive.5 小时前
redis实现漏桶算法--https://blog.csdn.net/m0_74908430/article/details/155076710
redis·算法·junit
-森屿安年-5 小时前
LeetCode 283. 移动零
开发语言·c++·算法·leetcode
北京地铁1号线5 小时前
数据结构:堆
java·数据结构·算法