本文为初学并查集记录
并查集简而言之,就是并和查:
》》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条边*/
}
}