森林与并查集

森林与并查集

何衍泓

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

  • 首先实现有路径压缩和权重的加权并查集
    1. 权重是对3取余后的结果
    2. 如果是0,平局
    3. 如果是1,A输B
    4. 如果是0,A赢B
    5. 注意如果反着走只需要减去权重即可,因为他们同属于一个余数系
  • 然后按照并查集的解题思路依次处理即可
  • 注意事项写在代码注释了
#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

  • 连通性判断:如果AB不同,BC不同,则AC相同
  • 实际上就是加权并查集,对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;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值