刷爆LeetCode系列
- [LeetCode:省份数量 + 等式方程的可满足性](#LeetCode:省份数量 + 等式方程的可满足性)
- github地址
- 前言
- 一、省份数量
- 二、等式方程的可满足性
LeetCode:省份数量 + 等式方程的可满足性
github地址
前言
本文用C++实现LeetCode并查集的应用相关题目:
- 省份数量
- 等式方程的可满足性
一、省份数量
题目描述
题目链接 :https://leetcode.cn/problems/bLyHh0/description/


题目与思路分析
目标分析:
-
题目本质 :共有n个城市
给定一个
n × n的邻接矩阵isConnected,其中:isConnected[i][j] == 1表示城市i和城市j直接相连- 相连具有传递性 :
i ~ j,j ~ k⇒i ~ k
要求求出有多少个互相连通的城市集合(省份) :给定一组城市,直接相连 或间接相连 的城市属于同一个省份,不相连的城市属于不同省份
-
最终返回省份的数量
思路 :转化为并查集问题:
- 每个城市是一个节点
- 直接相连或间接相连的城市属于同一个集合
- 最终省份数量 = 集合的个数
操作 :借助并查集结构维护连通关系
- 初始化:每个城市自成一个集合
- 遍历矩阵
- 若
isConnected[i][j] == 1,且 i 和 j 不属于同一个集合时 ,合并城市i和j。用下标 i 和 j 标识城市
- 若
- 最后统计并查集中根节点的个数,即为省份数量
代码实现
之前写好的并查集
cpp
class UnionFindSet
{
public:
// 用 n 个数字构造并查集, 0-n 为下标, vector 中存数字
// 存负数: 代表当前 下标所代表的元素 为根
// 存 >=0 的数: 该数为 当前元素的父节点的 下标
UnionFindSet(size_t n)
:_ufs(n, -1)
{}
// 提供如下成员函数
void Union(int index1, int index2) // 将两个成员合并成一个集合
{
int root1 = FindRoot(index1);
int root2 = FindRoot(index2);
if (root1 == root2) // 两元素的根相同,即本身就在同一个集合,则不合并
return;
// 数据量小的集合 合并到 数据量大的集合 _ufs[root] 越小(越负),集合越大
// 作为子树合并的集合,层数会增加一层,因此要将 大集合 合并到 小集合
if (_ufs[root1] > _ufs[root2])
std::swap(root1, root2);
// 合并的逻辑
_ufs[root1] += _ufs[root2]; //
_ufs[root2] = root1;
}
// 非压缩路径版本
//// 可以在边找的过程中,加一个压缩路径的过程
//int FindRoot(int index) // 找当前元素的根结点的下标
//{
// int parent = index; // 刚开始,每个元素都是一个独立的的根
// // 找根小于0的位置
// while (_ufs[parent] >= 0)
// {
// parent = _ufs[parent];
// }
// return parent;
//}
// 压缩路径版
int FindRoot(int index) // 找当前元素的根结点的下标
{
// 1. 找根 的下标
int root = index;
while (_ufs[root] >= 0)
{
root = _ufs[root];
}
// 2. 压缩路径
int cur = index;
while (_ufs[cur] >= 0)
{
int parent = _ufs[cur]; // 保存父亲
_ufs[cur] = root; // 将父亲改成跟
cur = parent; // 更新 cur
}
return root;
}
bool IsInSet(int index1, int index2) // 判断两个元素是否在同一个集合
{
int root1 = FindRoot(index1);
int root2 = FindRoot(index2);
return root1 == root2; // 两个元素有相同的根,则在同一个集合
}
// 数组中有几个值是负数,就有几个集合
size_t SetCount() const // 返回当前并查集中 集合的个数
{
size_t count = 0;
for (size_t i = 0; i < _ufs.size(); ++i)
{
if (_ufs[i] < 0)
++count;
}
return count;
}
private:
vector<int> _ufs;
};
直接复用写好的并查集
cpp
class Solution
{
public:
int findCircleNum(vector<vector<int>>& isConnected)
{
UnionFindSet ufs(isConnected.size());
for(int i = 0;i < isConnected.size();++i)
{
for(int j = 0;j < isConnected[i].size();++j)
{
if(isConnected[i][j] == 1)
{
// 把相连的城市放入同一个集合
ufs.Union(i, j);
}
}
}
return ufs.SetCount();
}
};
算法代码优化
-
难道做这道题我们要先写一个并查集吗,显然有些麻烦。
-
我们可以将并查集的核心思想和函数提取出来,直接在题解中使用
手动控制并查集
cpp
class Solution
{
public:
int findCircleNum(vector<vector<int>>& isConnected)
{
vector<int> ufs(isConnected.size(), -1);
auto FindRoot = [&ufs](int index)
{
// 找根
int root = index;
while(ufs[root] >= 0)
root = ufs[root];
// 路径压缩
int cur = index;
while(ufs[cur] >= 0)
{
int parent = ufs[cur];
ufs[cur] = root;
cur = parent;
}
return root;
};
// 相连或间接相连的城市属于一个省份,即属于同一个集合
// 返回省份的数量,即:返回集合的数量
for(int i = 0;i < isConnected.size();++i)
{
for(int j = 0;j < isConnected[i].size();++j)
{
if(isConnected[i][j] == 1)
{
// 第 i 个城市和第 j 个城市直接相连,表明同属于一个集合,则合并
// 手动实现并查集的合并逻辑
int root1 = FindRoot(i);
int root2 = FindRoot(j);
// 根不同时才合并
if(root1 != root2)
{
ufs[root1] += ufs[root2];
ufs[root2] = root1;
}
}
}
}
// 最终返回 并查集中 负数的个数
int count = 0;
for(auto e:ufs)
{
if(e < 0)
++count;
}
return count;
}
};
解释:
-
vector<int> ufs(isConnected.size(), -1);:并查集存储结构,初始化数组全为-1 -
FindRoot函数为Lambda表达式,引用捕获并查集ufs ,实现路径压缩版本的查找根的函数 -
相连或间接相连的城市属于一个省份,即属于同一个集合;
-
返回省份的数量,即:返回集合的数量
-
手动控制合并逻辑
cppif(isConnected[i][j] == 1) { // 第 i 个城市和第 j 个城市直接相连,表明同属于一个集合,则合并 // 手动实现并查集的合并逻辑 int root1 = FindRoot(i); int root2 = FindRoot(j); // 根不同时才合并 if(root1 != root2) { ufs[root1] += ufs[root2]; ufs[root2] = root1; } } -
并查集中负数的个数,即为集合的个数,即为省份数量
cpp// 最终返回 并查集中 负数的个数 int count = 0; for(auto e:ufs) { if(e < 0) ++count; } return count;
二、等式方程的可满足性
题目描述
题目链接 :https://leetcode.cn/problems/bLyHh0/description/


题目与思路分析
目标分析:
-
给定一个由表示变量之间关系的字符串方程组成的数组,每个字符串方程
equations[i]的长度为4,并采用两种不同的形式之一:"a==b"或"a!=b"。在这里,a 和 b 是小写字母(不一定不同),表示单字母变量名。只有当可以将整数分配给变量名,以便满足所有给定的方程时才返回
true,否则返回false。 -
判断所有的字符串等式所表示的关系,是否有违背
-
**所有字符串表示的关系均成立时,为
true,有一个违背时,为false**
思路 :并查集
- 首先遍历所有的等式,构造并查集。相等的字母,合并到同一个集合中
- 然后遍历所有的不等式,不等式两端的字母一定不属于同一个集合
通过观察得知:
equations[i][0]和equations[i][3]是小写字母equations[i][1]要么是'=',要么是'!'equations[i][2]是'='
操作:
-
构造26个小写字母的并查集并初始化 :
vector<int> ufs(26, -1); -
实现并查集的查找函数
FindRoot,注意对两端字母查找时传参为FindRoot(str[0] - 'a');和FindRoot(str[3] - 'a'); -
首先遍历表示相等的字符串:
if(str[1] == '='),查找时查找字母所对应的ASCII码 ,不属于同一个集合的两个字母进行合并cppfor(auto& str : equations) { if(str[1] == '=') { int root1 = FindRoot(str[0] - 'a'); // 存每个小写字母的ASCII码 int root2 = FindRoot(str[3] - 'a'); if(root1 != root2) { ufs[root1] += ufs[root2]; ufs[root2] = root1; } } } -
然后遍历表示不等的字符串:
if(str[1] == '!'),判断不相等两端的字母的ASCII码 ,是否在同一个集合,在的话返回falsecppfor(auto& str : equations) { if(str[1] == '!') { // 存每个小写字母的ASCII码 int root1 = FindRoot(str[0] - 'a'); int root2 = FindRoot(str[3] - 'a'); if(root1 == root2) return false; } } -
遍历结束后,返回true
代码实现
cpp
class Solution
{
public:
bool equationsPossible(vector<string>& equations)
{
// 先遍历相等的等式,把所有相等的字母放入一个集合
// 再遍历不相等的等式,如果两端字母属于一个集合,则 false
// 26 个字母的 ASCII 码的并查集
vector<int> ufs(26, -1); // 初始时,26个字母各自成一个集合
auto FindRoot = [&ufs](int index)
{
// 找跟
int root = index;
while(ufs[root] >= 0)
root = ufs[root];
// 压缩路径
int cur = index;
while(ufs[cur] >= 0)
{
int parent = ufs[cur];
ufs[cur] = root;
cur = parent;
}
return root;
};
// 0 1 2 3
// a ! = b
// 先把相等的字母的 ASCII码 放入一个集合
for(auto& str : equations)
{
if(str[1] == '=')
{
int root1 = FindRoot(str[0] - 'a'); // 存每个小写字母的ASCII码
int root2 = FindRoot(str[3] - 'a');
if(root1 != root2)
{
ufs[root1] += ufs[root2];
ufs[root2] = root1;
}
}
}
// 再把不相等的,判断是否在同一个集合
for(auto& str : equations)
{
if(str[1] == '!')
{
// 存每个小写字母的ASCII码
int root1 = FindRoot(str[0] - 'a');
int root2 = FindRoot(str[3] - 'a');
if(root1 == root2)
return false;
}
}
return true;
}
};
算法代码优化
- 有了第一道题的经验,这道题中我们直接使用手动控制并查集的方式实现,因此该题目无需优化
以上就是本文的所有内容了,如果觉得文章对你有帮助,欢迎 点赞⭐收藏 支持!如有疑问或建议,请在评论区留言交流,我们一起进步
分享到此结束啦
一键三连,好运连连!你的每一次互动,都是对作者最大的鼓励!
征程尚未结束,让我们在广阔的世界里继续前行!🚀