最近忙于找实习,许久未更新,本次分享有关并查集的几道题目~
并查集的实现
class UnionSet{
public:
UnionSet(int n):fa(n + 1){
for(int i = 0; i <= n; i++) fa[i] = i;//初始化父节点序列,每个序列的父节点为它自己
}
int get(int x){
return fa[x] = (fa[x] == x ? x : gei(fa[x]));//查找某个节点的根节点,在查找的同时剪枝
}
void merge(int a, int b){
fa[get(a)] = get(b);//将a节点的根节点设置为b节点的根节点
}
vector<int>fa;
};
由上述代码可以看到,其实并查集的核心代码只有三行(标有注释的三行)。
- 第一行是初始化父节点:数组fa代表的是每个几点的根节点,在初始化时让每个节点的父节点都变成其本身;
- 第二行是查找根节点:如果某个节点是根节点,则其父节点是其本身。通过这个逻辑查找根节点,在查找的同时实现剪枝,提高后续操作的效率;
- 第三行是连接两个节点:直接将a节点的根节点设置为b节点的根节点即可。
题目1:最长连续序列(leetcode - 128)
题目描述
给定一个未排序的整数数组 nums
,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。
请你设计并实现时间复杂度为 O(n)
的算法解决此问题。
样例输入
输入:nums = [100, 4, 1, 3, 2]
输出:4
解释:最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4。
题目分析
题目要求时间复杂度为O(n),因此不能用排序算法。可通过并查集,依次扫描集合中的每个元素,查找集合中是否有于其相邻的元素,有则将两个元素连接。
连接完成后,再将最长的子序列找出。
最终代码
class UnionSet {
public :
UnionSet(int n) : fa(n + 1), size(n + 1) {
for (int i = 0; i <= n; i++) {
fa[i] = i;
size[i] = 1;
}
}
int find(int x) {
return fa[x] = (fa[x] == x ? x : find(fa[x]));
}
int merge(int a, int b) {
int aa = find(a), bb = find(b);
if (aa == bb) return 0;
fa[aa] = bb;
size[bb] += size[aa];
return 1;
}
vector<int> fa, size;
};
class Solution {
public:
int longestConsecutive(vector<int>& nums) {
int n = nums.size(), cnt = 0;
unordered_map<int, int> h;
UnionSet u(n);
for (int i = 0; i < n; i++) {
int x = nums[i];
if (h.find(x) != h.end()) continue;
h[x] = (cnt++);
if (h.find(x + 1) != h.end()) u.merge(h[x], h[x + 1]);
if (h.find(x - 1) != h.end()) u.merge(h[x], h[x - 1]);
}
int ans = 0;
for (int i = 0; i < cnt; i++) {
ans = max(ans, u.size[i]);
}
return ans;
}
};
题目2:被围绕的区域(leetcode - 130)
题目描述
给你一个 m x n
的矩阵 board
,由若干字符 'X'
和 'O'
,找到所有被 'X'
围绕的区域,并将这些区域里所有的 'O'
用 'X'
填充。
样例输入
输入:board = [["X","X","X","X"],["X","O","O","X"],["X","X","O","X"],["X","O","X","X"]]
输出:[["X","X","X","X"],["X","X","X","X"],["X","X","X","X"],["X","O","X","X"]]
解释:被围绕的区间不会存在于边界上,换句话说,任何边界上的 'O' 都不会被填充为 'X'。 任何不在边界上,或不与边界上的 'O' 相连的 'O' 最终都会被填充为 'X'。如果两个元素在水平或垂直方向相邻,则称它们是“相连”的。
题目分析
可以认为存在一个虚拟外节点,如果某个节点'O'与虚拟外界点连通,则该点不被'X'包围,否则一定会被'X'包围。
可以依次扫描矩阵中的每个元素,如果是'X'则直接跳过,如果是'O',则首先判断是否是边缘的点,如果是则与虚拟外连通。同时,也要扫描该点的右方和下方是否存在'O'节点,存在的话则连通。
最终再依次扫描每个节点,如果是'O'节点, 则判断是否与虚拟外节点连通,连通的话则保留,否则替换为'X'。
最终代码
class UniontSet{//定义并查集
public:
UniontSet(int n):f(n + 1){
for(int i = 0; i <= n; i++){
f[i] = i;
}
}
int find(int n){
return f[n] = (f[n] == n ? n : find(f[n]));
}
void merge(int a, int b){
if(find(a) == find(b)) return ;
f[find(a)] = find(b);
return ;
}
vector<int>f;
};
class Solution {
public:
void solve(vector<vector<char>>& board) {
int n = board.size();
int m = board[0].size();
int ind;
UniontSet u(n * m);
for(int i = 0; i < n; i++){
for(int j = 0; j < m; j++){
if(board[i][j] == 'X') continue;
ind = i * m + j + 1;//这是一个表示矩阵中每个几点的小技巧
if(i == 0 || i == n - 1 || j == 0 || j == m - 1) u.merge(ind, 0);
if(j + 1 < m && board[i][j + 1] == 'O') u.merge(ind, ind + 1);
if(i + 1 < n && board[i + 1][j] == 'O') u.merge(ind, ind + m);
}
}
for(int i = 0; i < n; i++){
for(int j = 0; j < m; j ++){
if(board[i][j] == 'X') continue;
ind = i * m + j + 1;
if(u.find(ind) != u.find(0)) board[i][j] = 'X';
}
}
return ;
}
};
总结
并查集可以方便高效的解决存在连通关系的问题,且并查集的实现也非常简单,三行核心代码一定要理解并记熟。