等式方程的可满足性(并查集实现)
该文章记录了作者的思考过程,可能比较繁琐,如果想直接看答案思路的,直接看分析1.0,2.0以及最终代码。
先看题目:
给定一个由表示变量之间关系的字符串方程组成的数组,每个字符串方程 equations[i] 的长度为 4,并采用
两种不同的形式之一:"a==b" 或 "a!=b"。在这里,a 和 b 是小写字母(不一定不同),表示单字母变量名。
只有当可以将整数分配给变量名,以便满足所有给定的方程时才返回 true,否则返回 false。
示例 1:
输入:["a==b","b!=a"]
输出:false
解释:如果我们指定,a = 1 且 b = 1,那么可以满足第一个方程,但无法满足第二个方程。没有办法分配变量
同时满足这两个方程。
示例 2:
输入:["b==a","a==b"]
输出:true
解释:我们可以指定 a = 1 且 b = 1 以满足满足这两个方程。
示例 3:
输入:["a==b","b==c","a==c"]
输出:true
示例 4:
输入:["a==b","b!=c","c==a"]
输出:false
示例 5:
输入:["c==c","b==d","x!=z"]
输出:true
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/satisfiability-of-equality-equations
最初想法:
刚开始开这道题目的时候,第一时间想到的是用hash表存储,每对相等的两个元素都有一个
hash表,当遍历到相等的元素时,直接判断是否在hash表中,在则返回false。但是后面想了一下,很难实现,因为n个表达式,最多可能需要存2n-1个hash表,每个hash表的储值最多可达n-1,这个太大了。
分析1.0:
看了一下官方答案,才知道用并查集的思想去实现,主要思路是:
1.先遍历相等的表达式,如果他们没有相同的父节点,就将其中一个作为另外一个父节点;如果有相同父节点则跳过。
比如:a == b, b ==c, c ==d; 图形表示为:d->c->b->a;
用代码实是:parent[d]=c;parent[c]=b;parent[b]=a;
即将多个值相同的变量,连接起来,在该序列的任意变量,都可以通过循环获取其最终父节点(比如a)
2.再遍历不相等的表达式,只要该表达式的两个变量拥有共同的最终父节点,则证明他们相等返回false,如果没有冲突最后返回ture。
下面c++实现代码(还可优化,如果只想看最终答案,往下滑):
int findFa(int parent[], int x) {
// 通过递归获取最终父节点
return (parent[x] == x ? x : findFa(parent, parent[x]));
}
bool equationsPossible(vector<string>& equations) {
int parent[26];
int child[26];
for (int i = 0; i < 26; i++) {
parent[i] = i; // 父节点
child[i] = i; // 子节点
}
for (auto a : equations) {
if (a[1] == '=') {
int x1 = a[0] - 'a';
int y1 = a[3] - 'a';
int x = findFa(parent, x1);
int y = findFa(parent, y1);
// 父节点不同
if (x != y) {
// 该节点有父节点
if (x != x1) {
//获取 x1的最新节点对接y1的最终父节点
//比如: 已有a->b->c 和 d->e->f两个序列,遍历到: b->e时
//对接的应该是 c->d 即 a->b->c->d->e->f
parent[y] = findFa(child, x1);
child[findFa(child, x1)] = y;
}
else if (y != y1) {
parent[x] = findFa(child, y1);
child[findFa(child, y1)] = x;
}
else {
// 当 x1, x2 都没有父节点时,直接赋值
parent[y1] = x1;
child[x1] = y1;
}
}
}
}
for (auto a : equations) {
if (a[1] == '!') {
//如果有相同父节点,则矛盾,返回false
if (findFa(parent, a[0] - 'a') == findFa(parent, a[3] - 'a')) {
return false;
}
}
}
return true;
}
分析2.0:
其实我们只需要一个节点的最终父节点,所以路径经过的节点并不关心,所以我们可以采用路径压缩的方式,比如上段代码提到的:
已有a->b->c 和 d->e->f两个序列,遍历到: b->e时,我们可以直接将 两个父节点对接,即:
a->d->e->f;这样e的最终父节点是a, b的最终父节点也还是a,同样满足条件。如此可省略一个child数组。
最终实现代码是:
int findFa(int parent[], int x) {
while (x != parent[x]) { //用循环代替递归,减少使用堆栈空间。
parent[x] = parent[parent[x]]; // 进行路由压缩(只需要最终父节点,不需要在意中间节点)
x = parent[x];
}
return x;
}
bool equationsPossible(vector<string>& equations) {
int parent[26];
for (int i = 0; i < 26; i++) {
parent[i] = i;
}
for (auto eq : equations) {
if (eq[1] == '=') {
int fa1 = findFa(parent, eq[0] - 'a');
int fa2 = findFa(parent, eq[3] - 'a');
if (fa1 != fa2) {
parent[fa2] = fa1; //父节点对接父节点
}
}
}
for (auto eq : equations) {
if (eq[1] == '!') {
if (findFa(parent, eq[0] - 'a') == findFa(parent, eq[3] - 'a')) {
return false;
}
}
}
return true;
}
最后:已经写完了,如果有描述不够清晰或者错误之处,可留言交流。