并查集是一种树型的数据结构,用于处理一些不相交集合的合并问题。
并查集的主要操作有
1-合并两个不相交集合
2-判断两个元素是否属于同一个集合
3-路径压缩
并查集的精髓
1、Make_Set(x) 把每一个元素初始化为一个集合
初始化后每一个元素的父亲节点是它本身,每一个元素的祖先节点也是它本身(也可以根据情况而变)。
2、Find_Set(x) 查找一个元素所在的集合
查找一个元素所在的集合,其精髓是找到这个元素所在集合的祖先!这个才是并查集判断和合并的最终依据。
判断两个元素是否属于同一集合,只要看他们所在集合的祖先是否相同即可。
合并两个集合,也是使一个集合的祖先成为另一个集合的祖先。
3、Union(x,y) 合并x,y所在的两个集合
合并两个不相交集合操作很简单:
利用Find_Set找到其中两个集合的祖先,将一个集合的祖先指向另一个集合的祖先。
并查集的优化:
1、Find_Set(x)时 路径压缩
寻找祖先时我们一般采用递归查找,但是当元素很多亦或是整棵树变为一条链时,每次Find_Set(x)都是O(n)的复杂度,有没有办法减小这个复杂度呢?
答案是肯定的,这就是路径压缩,即当我们经过"递推"找到祖先节点后,"回溯"的时候顺便将它的子孙节点都直接指向祖先,这样以后再次Find_Set(x)时复杂度就变成O(1)了。
可见,路径压缩方便了以后的查找。
2、Union(x,y)时 按秩合并
即合并的时候将元素少的集合合并到元素多的集合中,这样合并之后树的高度会相对较小。
主要代码实现
int father[MAX]; /* father[x]表示x的父节点*/
int rank[MAX]; /* rank[x]表示x的秩*/
/* 初始化集合*/
void Make_Set(int x)
{
father[x] = x; //根据实际情况指定的父节点可变化
rank[x] = 0; //根据实际情况初始化秩也有所变化
}
/* 查找x元素所在的集合,回溯时压缩路径*/
int Find_Set(int x)
{
if (x != father[x])
{
father[x] = Find_Set(father[x]); //这个回溯时的压缩路径是精华
}
return father[x];
}
/*
按秩合并x,y所在的集合(即按照树形的高度,将高度低的合并到高的里面去)
下面的那个if else结构不是绝对的,具体根据情况变化
但是,宗旨是不变的即:按秩合并,实时更新秩。
*/
void Union(int x, int y)
{
x = Find_Set(x);
y = Find_Set(y);
if (x == y) return;
if (rank[x] > rank[y])
{
father[y] = x;
}
else
{
if (rank[x] == rank[y])
{
rank[y]++;
}
father[x] = y;
}
}
使用并查集查找时,如果查找次数很多,那么使用朴素版的查找方式肯定要超时。比如,有一百万个元素,每次都从第一百万个开始找,这样一次运算就是10^6,如果程序要求查找个一千万次,这样下来就是10^13,肯定要出问题的。
这是朴素查找的代码,适合数据量不大的情况:
int findx(int x) { int r=x; while(parent[r] !=r) r=parent[r]; return r; }
下面是采用路径压缩的方法查找元素:
int find(int x) //查找x元素所在的集合,回溯时压缩路径 { if (x != parent[x]) { parent[x] = find(parent[x]); //回溯时的压缩路径 } //从x结点搜索到祖先结点所经过的结点都指向该祖先结点 return parent[x]; }
上面是一采用递归的方式压缩路径, 但是,递归压缩路径可能会造成溢出栈,下面我们说一下非递归方式进行的路径压缩:
int find(int x)
{
int k, j, r;
r = x;
while(r != parent[r]) //查找跟节点
r = parent[r]; //找到跟节点,用r记录下
k = x;
while(k != r) //非递归路径压缩操作
{
j = parent[k]; //用j暂存parent[k]的父节点
parent[k] = r; //parent[x]指向跟节点
k = j; //k移到父节点
}
return r; //返回根节点的值
}
例题一:
题目来源:http://acm.hdu.edu.cn/showproblem.php?pid=1232
题目描述
Problem Description
某省调查城镇交通状况,得到现有城镇道路统计表,表中列出了每条道路直接连通的城镇。省政府“畅通工程”的目标是使全省任何两个城镇间都可以实现交通(但不一定有直接的道路相连,只要互相间接通过道路可达即可)。问最少还需要建设多少条道路?
Input
测试输入包含若干测试用例。每个测试用例的第1行给出两个正整数,分别是城镇数目N ( < 1000 )和道路数目M;随后的M行对应M条道路,每行给出一对正整数,分别是该条道路直接连通的两个城镇的编号。为简单起见,城镇从1到N编号。
注意:两个城市之间可以有多条道路相通,也就是说
3 3
1 2
1 2
2 1
这种输入也是合法的
当N为0时,输入结束,该用例不被处理。
Output
对每个测试用例,在1行里输出最少还需要建设的道路数目。
Sample Input
4 2
1 3
4 3
3 3
1 2
1 3
2 3
5 2
1 2
3 5
999 0
0
Sample Output
1
0
2
998
题目大意
根据某省城镇交通状况以及现有城镇道路统计表(表中列出了每条道路直接连通的城镇)问,使得全省任何两个城镇间都可以实现交通(但不一定有直接的道路相连,只要互相间接通过道路可达即可)。
解题思路
这是一道最赤果果的并查集问题。首先通过城镇的个数来确认至少需要多少条道路,然后将已有的道路输入,判断是否已有重复道路(如果没有重复,就将道路总数减一),最终得到最少还需要建设多少条道路。
代码如下:
#include <stdio.h>
#include <string.h>
int per[1010];
int n,m;
void prime()//初始化
{
int i;
for(i=1;i<=n;i++)
per[i]=i;
}
int find(int x)//找根节点
{
int r=x,i,j;
while(r!=per[r])
r=per[r];
return r;
}
bool join(int x,int y)//判断是否是一个集合
{
int fx=find(x);
int fy=find(y);
if(fx==fy)
return false;
else
per[fy]=fx;
return true;
}
int main()
{
int i,j,x,y;
int sum;
while(scanf("%d",&n),n)
{
sum=n-1;//道路的总数目
scanf("%d",&m);
prime();
for(i=0;i<m;i++)
{
scanf("%d%d",&x,&y);
if(join(x,y))
sum--;//减去已有道路数目
}
printf("%d\n",sum);
}
return 0;
}
例题二:
题目来源:http://acm.hdu.edu.cn/showproblem.php?pid=1272
题目描述
%�� style="color:rgb(124,169,237)">Problem Description
上次Gardon的迷宫城堡小希玩了很久(见Problem B),现在她也想设计一个迷宫让Gardon来走。但是她设计迷宫的思路不一样,首先她认为所有的通道都应该是双向连通的,就是说如果有一个通道连通了房间A和B,那么既可以通过它从房间A走到房间B,也可以通过它从房间B走到房间A,为了提高难度,小希希望任意两个房间有且仅有一条路径可以相通(除非走了回头路)。小希现在把她的设计图给你,让你帮忙判断她的设计图是否符合她的设计思路。比如下面的例子,前两个是符合条件的,但是最后一个却有两种方法从5到达8。
Input
输入包含多组数据,每组数据是一个以0 0结尾的整数对列表,表示了一条通道连接的两个房间的编号。房间的编号至少为1,且不超过100000。每两组数据之间有一个空行。
整个文件以两个-1结尾。
Output
对于输入的每一组数据,输出仅包括一行。如果该迷宫符合小希的思路,那么输出"Yes",否则输出"No"。
Sample Input
6 8 5 3 5 2 6 4
5 6 0 0
8 1 7 3 6 2 8 9 7 5
7 4 7 8 7 6 0 0
3 8 6 8 6 4
5 3 5 6 5 2 0 0
-1 -1
Sample Output
Yes
Yes
No
题目大意
判断设计图是否符合任意两个房间有且仅有一条路径可以相通(除非走了回头路)。
解题思路
首先将道路依次输入,然后判断是否有重复道路,如果有,那怕是只有一条重复道路,即为不符合要求。
代码如下:
#include <stdio.h>
#include <string.h>
#define N 100010
int per[N];
int vis[N];//做标记
bool flag;
void prime()//初始化
{
for(int i=1;i<N;i++)
per[i]=i;
memset(vis,0,sizeof(vis));
}
int find(int x)
{
int r=x;
while(r!=per[r])
r=per[r];
return r;
}
bool join(int x,int y)
{
int fx=find(x);
int fy=find(y);
if(fx==fy)
flag=false;//表示已成环
else
per[fy]=fx;
}
int main()
{
int a,b;
while(~scanf("%d%d",&a,&b))
{
flag=true;
if(a==-1&&b==-1)
break;
if(a==0&&b==0)//特殊数据
{
puts("Yes");
continue;
}
prime();
vis[a]=vis[b]=1;
join(a,b);
while(scanf("%d%d",&a,&b),a||b)
{
vis[a]=vis[b]=1;
join(a,b);
}
int num=0;
for(int i=1;i<N;i++)
{
if(vis[i]&&i==per[i])//被标记的根节点是自己
num++;
if(num>1)
{
flag=false;
break;
}
}
if(flag)
puts("Yes");
else
puts("No");
}
return 0;
}