哈喽各位,我是前端小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 = 验证等价关系。
通过巧妙的两遍扫描(先建关系,后查冲突),我们轻松破解了这个逻辑谜题。
在下一篇中,我们将挑战一个更加复杂、更贴近实际业务的并查集应用------"账户合并"。当一个用户拥有多个邮箱,而不同的邮箱列表又存在交集时,我们该如何理清这些混乱的账户关系?
下期见!