并查集入门

先看一道题:亲戚 - 洛谷 

  

题目背景

若某个家族人员过于庞大,要判断两个是否是亲戚,确实还很不容易,现在给出某个亲戚关系图,求任意给出的两个人是否具有亲戚关系。

题目描述

规定:x 和 y 是亲戚,y 和 z 是亲戚,那么 x 和 z 也是亲戚。如果 x,y 是亲戚,那么 x 的亲戚都是 y 的亲戚,y 的亲戚也都是 x 的亲戚。

输入格式

第一行:三个整数 n,m,p,(n,m,p≤5000),分别表示有 n 个人,m 个亲戚关系,询问 p 对亲戚关系。

以下 m 行:每行两个数 Mi​,Mj​  1≤Mi​, Mj​≤N,表示Mi​ 和 Mj​ 具有亲戚关系。

接下来 p行:每行两个数 Pi,P j,询问 Pi Pj​ 是否具有亲戚关系。

输出格式

pp 行,每行一个 Yes 或 No。表示第 ii 个询问的答案为“具有”或“不具有”亲戚关系。

输入输出样例

6 5 3
1 2
1 5
3 4
5 2
1 3
1 4
2 3
5 6

输出 #1复制

Yes
Yes
N

 这道题的一个暴力做法就是把这个图储存起来:

 我们可以采用链式前向星的方式把图存好;

按照题目要求进行dfs遍历:从一个点搜索,看能不能搜到另一个点;

时间复杂度: dfs O(n)    一个M,一个p,  所以总的时间复杂度为O(n*p+m),好像能过:

代码:

#include<iostream>
#include<cstring>
#include<queue>
using std::queue;
const int N = 1e4+7;
struct E {
	int to, w, ne;
};
E edge[N];
int h[N], cnt;
bool vis[N];
void add(int u, int v, int ww=0) {
	edge[cnt].to = v;
	edge[cnt].w = ww;
	edge[cnt].ne = h[u];
	h[u] = cnt;
	cnt++;
}
bool flag = 0;
void dfs(int u,int aim) {
	if (u == aim) {
		flag = 1;
		return;
	}
	if (h[u] == -1 ||vis[u] ) return ;

	vis[u] = true;
	for (int i = h[u]; i != -1; i = edge[i].ne) {
		dfs(edge[i].to,aim);
	}
}
int main() {
	memset(h, -1, sizeof h);
	int n, m, p;
	std::cin >> n >> m >> p;
	for (int i = 0; i < m; i++) {
		int m1, m2;
		std::cin >> m1 >> m2;
		//无向图;
		add(m1, m2);
		add(m2, m1);
	}
	while (p--) {
		memset(vis, 0, sizeof vis);
		int p1, p2;
		std::cin >> p1 >> p2;
		flag = 0;
		dfs(p1, p2);
		if (flag)std::cout << "Yes\n";
		else std::cout << "No\n";	
	}
	return 0;
}

但如果 n和p非常的大呢,n为1e5呢; 那肯定就过不了了;

接下来介绍一个代码十分好写,并且容易理解的算法:

并查集:

对于一个集合A和一个集合B;如果要把集合A与集合B合并起来,最暴力的方法时间复杂度为O(n)

但如果可以找的A的“特征元素”a  使B的特征元素b构建关系,只要a,b是有联系的,那么我们可以认为A集合和B集合再同一个集合内;那么a和b就是这两个集合的桥梁;那么把A集合与B集合合并

就是使a与b有联系;

图1:

以树的方式理解:

图2:

 若我们要把根为1的树,与根为9的树合并,1,9便是这特征元素,只需把9移到1

 像这样就成功的合并了两个集合;合并时间复杂度为O(1);

如果我们要元素x和元素y建立关系,

就先判断x所在的树的根,与y是否相等

如果相等,那么就无需合并,因为它们在同一个集合内;

如果不等就合并;

但是我们怎么找到一个元素所在树的树根呢?

我们可以构建一个映射关系,以图二为例;1

找5的树根:5的父节点为3,3的父节点为1,1没有父节点,所以1是树根

找10的树根,10->8->9; 所以9是树根

因此可以构建这样的数组 fa[x]为x的父节点,if  fa[x]==x 它的父节点是它自己;

再来找一次5的树根:

fa[5]=3,  fa[3]=1 fa[1]=1;

那么写成代码可以这样表示:

int find(int x){
   if(x==fa[x])return x;
   else return find(fa[x]);
}

如果要合并5和10的区间: 

//由 find(5)==fa[find(5)]
//得
fa[find(5)]=find(10)
//或
fa[find(10)]==find(5);

 图二左右两边的树的就是这样构建而来的;这时我们是没有考虑谁来当父节点或者是谁来当树根的;

如果要构建一个完整的关系图,每个元素就必须先要初始化为f[x]=x;才能保证它有树根

再来看一下刚开始那到题:

1 2

1 5

3 4

5 2

1 3

模拟一下树的构建:

fa[find(1)]=find(2)    {因为初始化了fa[x],所以刚开始,find[x]=x,也就是它自己;}

 

fa[find(1)]=find(5);

 fa[find(3)]=find(4);

 fa[find(5)]=find(2);//没变;

 fa[find(1)]=find(3)

 

 没了最后还差个6

 

 图就这样构建完了;

 

再看那道题的要求: 询问两个人是否为亲戚关系:即这两个点是否连通:

1 4

2 3

5 6

find(1) ==4,  find(4)==4  相等所以连通;

find(2)==4,  find(3)==4,  连通;

find(5)==4,  find(6)==6  不相等,所以不连通

还有一个需要思考的问题是:find函数的时间复杂度问题:

如果是这样的:

那么就需要O(n)的复杂度了,

概率比较大的情况就是二叉树,时间复杂度为O(log(n));

这种不稳定的查找是可以优化的并且很简单:

find(1):

 通过一路的查找顺带将子节点迁到树根下面

这种方法叫做路径压缩

代码:

int find(int x){
return x==f[x]?x:f[x]=find(f[x]);
}
//或:
int find(int x){
while(x!=f[x])x=f[x]=find(f[x]);
return x;
}

以上就是并查集的大概思想了:

给出完整代码:

#include<iostream>
const int N = 5e3 + 7;
int f[N];
void init() { //初始化
	for (int i = 1; i <= N; i++)f[i] = i;
}
int find(int x) {
	return x == f[x] ? x : f[x] = find(f[x]);
}
int main() {
	std::ios::sync_with_stdio(false);
	std::cin.tie(0);
	int n, m, p;
	std::cin >> n >> m >> p;
	init();
	while (m--) {
		int m1, m2;
		std::cin >> m1 >> m2;
       //构建关系;
		f[find(m1)] = find(m2);
	}
	while (p--) {
		int p1, p2;
		std::cin >> p1 >> p2;
		if (find(p1) == find(p2))std::cout << "Yes\n";
		else std::cout << "No\n";
	}
	return 0;
}

分享几个题,并给出思路:

 集合 - 洛谷  //可以通过筛来构建图;并且要用无优化的筛;

朋友 - 洛谷 //可以用map 也可以将普通数组开到数据的2倍大小来转换女方的负数;并且这道题是需要维护树根,因为fa[find(x)]=find(y)右边为树根,所以只需将某个节点一直放右边即可;

  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值