并查集简述---2021/03/13

本文为初学并查集记录

并查集简而言之,就是并和查:
》》1)并:为将两个集合(集合可为单个元素)合并;
》》2)查:查找节点的父节点;

举例:

有五个人(A,B,C,D,E),现在我们强行让他们互相交朋友:
先让A和B交朋友,就是将A和B合并在一个集合里,我们用{A,B}表示。
再让B和C交朋友,就是将B和C合并在一个集合里,又因为{A,B}已经在一个集合里了,那么实际上,ABC就在同一个集合里{A,B,C}。
用自然语言表达是这样的,那么用代码呢?

我们不妨开一个数组vis[],让数组一开始初始化为自己对应的下标,比如:
vis[0]=0; vis[1]=1; vis[2]=2;
然后A,B,C,D,E对应0,1,2,3,4
————————————————————————————————
然后我们将他们看成一个一个的点
如果将A和B合并起来
》我们就改变vis[]里的值,让他们不再指向自己,而是其中一个人指向另一个人
比如:vis[0]=1; (0是A的下标,1是B的下标);
于是A和B就被我们放在了一个集合里。
在这里插入图片描述

那么如果我们要将B和C合并在一个集合里,实际上是集合{A,B}和集合{C}的合并,我们可以直接改变vis[1]=2吗?
看起来是可以的,但是假设我们是合并A和C呢,我们将vis[0]=2,那么A和C是合并起来了,但是A和B直接的关系却丢失了。

那么我们引进
查:查找节点的父节点
当我们将两个集合并到一起的时候,我们是查找节点的父节点,通过修改父节点的指向。
即vis[find(0)]=2;
在这里插入图片描述
find函数是我们定义的一个函数,他的作用就是帮助我们找到节点的最顶层的祖宗。
事实上,我们可以让A和C合并的时候,也去找C的最顶层祖宗,这样后面寻找原本集合A的根节点的父节点时,就只需要走一层。
即:vis[find(A)]=find(B)
于是,我们能够知道,在一个集合里,应当只有一个节点时指向自己的,我们称它为根节点。
事实上,在这里一个集合,就是一棵树,判断有几个集合,只需要遍历vis检查有多少个根节点即可。

代码:
首先是find(int x)函数

int find(int x)
{
	if(vis[x]!=x)vis[x]=find(vis[x]);	//在这里使用了递归和路径压缩
	return vis[x];
}

路径压缩:顾名思义,就是当我们一层一层往上找顶层祖宗的时候,找到顶层祖宗后,直接让路过的所有点指向顶层祖宗,在顶层祖宗不变的情况下,下次找祖宗的时候,我们只需要走一层就找到了。

eg:
就是现在A的顶层祖宗是A的曾爷爷
A找顶层祖宗的时候:
A要去找A爸
然后让A爸去找A爸的爸爸
然后让A爸的爸爸(就是A的爷爷)去找他爸
然后发现曾爷爷是顶层祖宗了
就结束寻找
一层一层回来:
A曾爷爷告诉A爷爷,他是顶层祖宗
A爷爷告诉A爸爸,A曾爷爷是顶层祖宗
A爸爸告诉A,A曾爷爷是顶层祖宗
然后A就知道,A曾爷爷是顶层祖宗

!!!然后路径压缩,从这个例子上来讲有点违背伦理,但是程序没有伦理。
所谓路径压缩就是:
A爷爷认A曾爷爷做父亲,A爸爸也认A曾爷爷做父亲,A也认A曾爷爷做父亲。
然后A原本的爷爷,爸爸和自己,就做了兄弟,找顶层祖宗的时候,只需要找一层就够了。

表现到代码里就是:

//vis数组一开始的状态
//vis[]={0,2,3,4,4};
//即下标为4的元素是根节点(顶层祖宗)
//路径压缩后:
//vis[]={0,4,4,4,4};

用图表示就是:

事实上到这里,你已经学会了查的操作,至于并的操作,相信你已经有想法了。
实际上,到代码里,就一行:

	vis[find(a)]=find(b);

于是,我们开始实践一下:

题目为:hdu1232

问题:

某省调查城镇交通状况,得到现有城镇道路统计表,表中列出了每条道路直接连通的城镇。省政府“畅通工程”的目标是使全省任何两个城镇间都可以实现交通(但不一定有直接的道路相连,只要互相间接通过道路可达即可)。问最少还需要建设多少条道路?

输入:

测试输入包含若干测试用例。每个测试用例的第1行给出两个正整数,分别是城镇数目N ( < 1000 )和道路数目M;随后的M行对应M条道路,每行给出一对正整数,分别是该条道路直接连通的两个城镇的编号。为简单起见,城镇从1到N编号。
注意:两个城市之间可以有多条道路相通,也就是说
3 3
1 2
1 2
2 1
这种输入也是合法的
当N为0时,输入结束,该用例不被处理。

样例:

对每个测试用例,在1行里输出最少还需要建设的道路数目。

输入样例:

4 2
1 3
4 3
3 3
1 2
1 3
2 3
5 2
1 2
3 5
999 0
0

输出样例:

1
0
2
998

题解:

要注意的是,这道题的城镇序号是从1开始的,以及N不能为0,但是M可以为0。

事实上我的写法,感觉很暴力,简述我的思路就是:

每输入一条道路,我就更改关系,让他们并到一个集合里。

当输入结束的时候,我就只需要检查有几个集合(就是检查有几个根节点)就能知道至少需要几条路。

比如有五个城镇1,2,3,4,5

1,2,3在一个集合里;
4,5在一个集合里;

那么我只要在加一条路,就能让1,2,3,4,5连在一起。比如3和4之间加一条路,那么1,2,3,4,5就互通了(间接互通也算互通)。

那么思路已有,上C++代码:

#include <iostream>
#define L 1001
using namespace std;

int vis[L];		//表示关系的数组

void Init()		//初始化函数,初始化关系数组
{
	for(int i=1;i<L;i++)
		vis[i]=i;	//从1开始是因为城镇序号从1开始
}

int find(int x)	//找顶层祖宗函数
{
	if(vis[x]!=x)vis[x]=find(vis[x]);
	return vis[x];
}

int main()		//主函数
{
	int N,M;			//对应题目的N,M
	while(cin>>N>>M)	//读入N,M以及有多组测试样例
	{
		Init();				//初始化关系数组vis
		int node1,node2;	//用来存放节点
		for(int i=0;i<M;i++)
		{
			cin>>node1>>node2;
			vis[find(node1)]=find(node2);
		}
		int flag=0;	//用来保存有几个集合
		for(int i=1;i<=N;i++)
		{
			if(vis[i]==i)		//如果指向自己,那么这个节点就是根节点(顶层祖宗)
				flag++;			//记录集合数
		}
		cout<<flag-1<<endl;		//输出需要的最少道路
		
		/*实际上这里减一是树的一个特性,在后面的最小生成树中会用到
		 就是如果一个树有n个节点,那么他就有n-1条边
		假设这里是两个集合,我们可以把这两个集合看成是大一点的点
	   那么将两个节点连接起来生成一棵树,就需要1条边*/
	}
}
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值