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

哈喽各位,我是前端小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。

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

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

  3. 看 a==c:连通 a 和 c。 这就漏掉了矛盾!因为 a 和 c 连通后,通过 c 这个中介,a 和 b 其实也连通了,这就违背了第一条 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)。

      • 如果相等,说明 u 和 v 在之前的等式逻辑中已经被判定为"相等"了,现在你又说它俩不等,这就是逻辑矛盾 !直接返回 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 = 验证等价关系。

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

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

下期见!

相关推荐
dialect_1 小时前
KMP算法
开发语言·c++·算法
reikocao1 小时前
卡尔曼滤波的动态机制:P如何改变融合权重
算法
漂流瓶jz2 小时前
UVA-1612 猜名次 题解答案代码 算法竞赛入门经典第二版
数据结构·算法·题解·排序·aoapc·算法竞赛入门经典
吞下星星的少年·-·3 小时前
Div.2 CF 1130B题(构造,贪心)
算法·贪心·构造
是main不是漫4 小时前
【双指针——移动零】数组划分类题目的入门基石
c++·算法
深圳市快瞳科技有限公司5 小时前
野外鸟类AI识别落地实录:输入适配、结构化输出、去重聚合与置信度分流的工程实践
算法·计算机视觉·鸟类识别·鸟类ai识别·野外鸟类观测·林业调查
专注仿真6 小时前
梯度提升类(XFBoost)算法详解与代码实例
开发语言·人工智能·python·算法·大模型训练·控制算法
小豆庄6 小时前
视频遮罩追踪退场后ID撞号的排查—多目标场景下的失效
opencv·算法·bug·视频·python3.11
Tisfy7 小时前
LeetCode 3882.网格图中最小异或路径:动态规划
算法·leetcode·动态规划
D e re k ZH8 小时前
P5025 [SNOI2017] 炸弹
c++·算法