森林与并查集
何衍泓
Quick_Find 算法
- 基于染色思想,将连接的两个点染成相同的颜色
- 查找时间复杂度 O(1)
- 合并时间复杂度 O(n)
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<algorithm>
using namespace std;
#define MAX_N 1000
int color[MAX_N + 5] = { 0 };
void init(int n)//初始化n个节点的颜色
{
for (int i = 0; i < n; i++) color[i] = i;
}
int find(int a)
{
return color[a];
}
int merge(int a, int b, int n)//将a,b两个点连在一起
{
if (find(a) == find(b)) return 0;
int aa = color[a], bb = color[b];
for (int i = 0; i < n; i++)
{
if (color[i] == aa) color[i] = bb;
}
return 1;
}
void output(int n)
{
int ret = 0;
for (int i = 0; i < n; i++)
{
ret += printf("%3d ", i);
}
printf("\n");
for (int i = 0; i < ret; i++) printf("-");
printf("\n");
for (int i = 0; i < n; i++)
{
ret += printf("%3d ", color[i]);
}
printf("\n");
}
int main()
{
int a, b, n;
cin >> n;
init(n);
output(n);
while (cin >> a >> b)
{
printf("merge %d with %d = %d\n", a, b, merge(a, b, n));
output(n);
printf("\n\n");
}
return 0;
}
Quick_Union 算法
- 没有必要对每个节点都进行染色
- 将同个集合中的节点连接成一个树
- 每次连接的时候找到他的根节点
- 然后改变根节点的值即为将一个树看成另外一个数的子树
- 合并时间复杂度 O(1)
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<algorithm>
using namespace std;
#define MAX_N 1000
int fa[MAX_N + 5];
void init(int n)
{
for (int i = 0; i < n; i++)
{
fa[i] = i;
}
}
int find(int a)
{
if (a == fa[a]) return a;
return find(fa[a]);
}
int merge(int a, int b, int n)
{
int aa = find(a), bb = find(b);
if (aa == bb) return 0;
fa[aa] = bb;
return 1;
}
void output(int n)
{
int ret = 0;
for (int i = 0; i < n; i++)
{
ret += printf("%3d ", i);
}
printf("\n");
for (int i = 0; i < ret; i++) printf("-");
printf("\n");
for (int i = 0; i < n; i++)
{
printf("%3d ", fa[i]);
}
}
int main()
{
int n, a, b;
cin >> n;
init(n);
output(n);
while (cin >> a >> b)
{
printf("merge %d with %d : %d\n", a, b, merge(a, b, n));
output(n);
printf("\n\n");
}
return 0;
}
按秩优化和路径压缩后的 Quick_Union 算法
- 按秩优化:将节点少的树作为节点多的数的子树
- 路径压缩:find 的时候将遍历过的节点全部接到根节点下面
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<cstdio>
#include<cstdlib>
using namespace std;
#define MAX_N 1000
int fa[MAX_N + 5];
int Size[MAX_N + 5];
void init(int n)
{
for (int i = 0; i < n; i++)
{
fa[i] = i;
Size[i] = 1;
}
}
int find(int a)
{
return fa[a] = (fa[a] == a ? a : find(fa[a]));
}
int merge(int a, int b, int n)
{
int aa = find(a), bb = find(b);
if (aa == bb) return 0;
if (Size[aa] < Size[bb])
{
fa[aa] = bb;
Size[bb] += Size[aa];
}
else
{
fa[bb] = aa;
Size[aa] += Size[bb];
}
return 1;
}
void output(int n)
{
int ret = 0;
for (int i = 0; i < n; i++)
{
ret += printf("%3d ", i);
}
printf("\n");
for (int i = 0; i < ret; i++) printf("-");
printf("\n");
for (int i = 0; i < n; i++)
{
printf("%3d ", fa[i]);
}
printf("\n");
for (int i = 0; i < ret; i++) printf("-");
printf("\n");
for (int i = 0; i < n; i++) printf("%3d ", Size[i]);
}
int main()
{
int n, a, b;
cin >> n;
init(n);
output(n);
while (cin >> a >> b)
{
printf("merge %d with %d : %d\n", a, b, merge(a, b, n));
output(n);
printf("\n\n");
}
return 0;
}
最长连续序列
https://leetcode.cn/problems/longest-consecutive-sequence/description/
- 从前往后遍历nums数组
- 如果在前面出现过这个数字就处理
- 同时将数字用哈希表转化为数组下标
- 封装一个并查集UnionSet的类
- 在类内使用优化后的并查集
- 主函数里面如果能找到前面有和当前数字联通的就使用并查集的merge方法
- 最后找到最大的size即可,就是最大的集合
class Unionset{
public:
//init Unionset
Unionset(int n) : fa(n+1),size(n+1)
{
for(int i=0;i<=n;i++)
{
fa[i] = i;
size[i] = 1;
}
}
//加上路径压缩的find方法
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();
int 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;
}
};
被围绕的区域
https://leetcode.cn/problems/surrounded-regions/description/
- 无论如何连接体问题先把并查集类写出来
- 假设最外面的一圈是0
- 特殊把边界的O和0连接起来
- 然后判断父节点是否一样即可判断联通了
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 : get(fa[x]));
}
void merge(int a,int b){
if(get(a) == get(b)) return ;
fa[get(a)] = get(b);
return ;
}
vector<int> fa;
};
class Solution {
public:
void solve(vector<vector<char>>& board) {
int n = board.size(),m = board[0].size();
UnionSet u(n*m);
for(int i=0;i<n;i++)
{
for(int j=0;j<m;j++)
{
if(board[i][j] != 'O') continue;
int ind = m * i + j + 1;
if(i == 0 || i == n - 1) u.merge(ind,0);
if(j == 0 || j == m - 1) u.merge(ind,0);
if(i < n - 1 && board[i+1][j] == 'O') u.merge(ind,ind+m);
if(j < m - 1 && board[i][j+1] == 'O') u.merge(ind,ind+1);
}
}
for(int i=0;i<n;i++)
{
for(int j=0;j<m;j++)
{
if(board[i][j] != 'O') continue;
int ind = m * i + j + 1;
if(u.get(ind) != u.get(0)) {
board[i][j] = 'X';
}
}
}
return ;
}
};
岛屿数量
https://leetcode.cn/problems/number-of-islands/description/
- 与上一题类似的解题思路
- 先实现 UnionSet 的类
- 然后遍历做联通即可,注意下标的映射
- 然后再遍历一遍,如果是‘1’并且是根节点就是一个并查集集合
- 返回统计答案
class UnionSet{
public:
UnionSet(int n) : fa(n + 1){
for(int i=0;i<=n;i++){
fa[i] = i;
}
}
int get(int a){
return fa[a] = (fa[a] == a ? a : get(fa[a]));
}
void merge(int a,int b){
fa[get(a)] = get(b);
return ;
}
vector<int> fa;
};
class Solution {
public:
int numIslands(vector<vector<char>>& grid) {
int n = grid.size(), m = grid[0].size();
UnionSet u(n * m);//注意这个大小要开 n * m
for(int i=0;i<n;i++){
for(int j=0;j<m;j++){
if(grid[i][j] == '0') continue;
int ind = i * m + j + 1;//下标映射
if(j + 1 < m && grid[i][j + 1] == '1'){
u.merge(ind, ind + 1);
}
if(i + 1 < n && grid[i + 1][j] == '1'){
u.merge(ind, ind + m);
}
}
}
int ans = 0;
for(int i=0;i<n;i++){
for(int j=0;j<m;j++){
int ind = i * m + j + 1;
if(grid[i][j] == '1' && u.get(ind) == ind){
ans++;
}
}
}
return ans;
}
};
省份数量
https://leetcode.cn/problems/number-of-provinces/description/
- 没有难度,直接参考上一题即可
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 : get(fa[x]));
}
void merge(int a,int b){
fa[get(a)] = get(b);
return ;
}
vector<int> fa;
};
class Solution {
public:
int findCircleNum(vector<vector<int>>& isConnected) {
int n = isConnected.size(), m = isConnected[0].size();
UnionSet u(n * m);
for(int i=0;i<n;i++){
for(int j=0;j<m;j++){
if(isConnected[i][j] == 0) continue;
u.merge(i,j);
}
}
int ans = 0;
for(int i=0;i<n;i++){
if(u.get(i) == i) ans++;
}
return ans;
}
};
猜拳(加权并查集)
https://oj.haizeix.com/problem/72
- 首先实现有路径压缩和权重的加权并查集
- 权重是对3取余后的结果
- 如果是0,平局
- 如果是1,A输B
- 如果是0,A赢B
- 注意如果反着走只需要减去权重即可,因为他们同属于一个余数系
- 然后按照并查集的解题思路依次处理即可
- 注意事项写在代码注释了
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<algorithm>
#include<string>
#include<vector>
using namespace std;
//猜拳
struct WeightedUnionSet {//加权并查集类
public:
WeightedUnionSet(int n) : fa(n + 1), val(n + 1) {
for (int i = 0; i <= n; i++) {
fa[i] = i;
val[i] = 0;
}
}
int get(int x) {
if (fa[x] == x) return x;
int root = get(fa[x]);
val[x] = (val[x] + val[fa[x]]) % 3;
return fa[x] = root;//同时做赋值和放回root
}
void merge(int a, int b, int t) {
int aa = get(a), bb = get(b);//取到二者根节点
if (aa == bb) return;
val[aa] = (t + val[b] - val[a] + 3) % 3;//加3保证是正数
fa[aa] = bb;
return;
}
vector<int> fa, val;
};
int main()
{
int n, m;
cin >> n >> m;
WeightedUnionSet u(n);
for (int i = 0, a, b, c; i < m; i++) {
cin >> a >> b >> c;
if (a == 1) {
u.merge(b, c, 2);//胜利为2
}
else {
if (u.get(b) != u.get(c)) {//经过这个判断之后已经路径压缩了
cout << "Unknown" << endl;
}
else {
int op = (u.val[b] - u.val[c] + 3) % 3;
switch (op)
{
case 0:cout << "Tie" << endl; break;
case 1:cout << "Loss" << endl; break;
case 2:cout << "Win" << endl; break;
}
}
}
}
return 0;
}
程序自动分析
https://oj.haizeix.com/problem/322
- 本题数据过大记得用哈希表做下标映射,不然容易Runtime Error
- 下标映射设置cnt即可,要是前面没出现过就分配一个当前cnt值给他
- 等于关系可以连通,判断不等于关系是否在一个连通集里面即可
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<algorithm>
#include<unordered_map>
#include<string>
#include<vector>
using namespace std;
//程序自动分析
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 : get(fa[x]));
}
void merge(int a, int b) {
fa[get(a)] = get(b);
return;
}
vector<int> fa;
};
struct Data {
int a, b, e;
}arr[1000000];
int main()
{
int t;
int n;
cin >> t;
while (t--) {
cin >> n;
int flag = 1;
UnionSet u(2 * n);
//数据过大,利用哈希表做下标映射
unordered_map<int, int> h;
int cnt = 0;
for (int i = 0; i < n; i++)
{
cin >> arr[i].a >> arr[i].b >> arr[i].e;
//下标分配
if (h.find(arr[i].a) == h.end()) h[arr[i].a] = cnt++;
if (h.find(arr[i].b) == h.end()) h[arr[i].b] = cnt++;
if (arr[i].e == 1) u.merge(h[arr[i].a], h[arr[i].b]);
}
for (int i = 0; i < n; i++)
{
if (arr[i].e == 1) continue;
if(u.get(h[arr[i].a]) == u.get(h[arr[i].b])) flag = 0;
}
if (flag) cout << "YES" << endl;
else cout << "NO" << endl;
}
return 0;
}
关押罪犯(加权并查集)
https://oj.haizeix.com/problem/327
- 连通性判断:如果A与B不同,B与C不同,则A与C相同
- 实际上就是加权并查集,对2取余,权重1代表不同,0代表相同
- 将各个关系都放入加权并查集即可,冲突值从大到小排序,依次处理
- 处理到第一个同个监狱的即为答案
- 若一直没有就输出0即可
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<algorithm>
#include<unordered_map>
#include<string>
#include<vector>
using namespace std;
//关押罪犯
class WeightedUnionSet {
public:
WeightedUnionSet(int n) : fa(n + 1), val(n + 1) {
for (int i = 0; i <= n; i++) {
fa[i] = i;
val[i] = 0;
}
}
int get(int x) {
if (fa[x] == x) return x;
int root = get(fa[x]);
val[x] = (val[x] + val[fa[x]]) % 2;
return fa[x] = root;
}
void merge(int a, int b, int t) {
int aa = get(a), bb = get(b);
if (aa == bb) return;
val[aa] = (val[b] + t - val[a] + 2) % 2;
fa[aa] = bb;
}
vector<int> fa, val;
};
struct Data {
int a, b, c;
}arr[100000];
int main()
{
int n, m;
cin >> n >> m;
WeightedUnionSet u(n);
for (int i = 0; i < m; i++)
{
cin >> arr[i].a >> arr[i].b >> arr[i].c;
}
sort(arr, arr + m, [&](const Data& a, const Data& b)->bool {
return a.c > b.c;
});
int ans = 0;
for (int i = 0; i < m; i++)
{
if (u.get(arr[i].a) == u.get(arr[i].b)) {//如果a和b的关系知道
if ((u.val[arr[i].a] - u.val[arr[i].b] + 2) % 2 == 0) {
ans += arr[i].c;
break;
}
}
else {
u.merge(arr[i].a, arr[i].b, 1);//第一次出现分配到不同牢房
}
}
cout << ans << endl;
return 0;
}