并查集是一种树型的数据结构,用于处理一些不相交集合(Disjoint Sets)的合并及查询问题。能够实现较快的合并和判断元素所在集合的操作,
应用很多,如其判定一个无向图是否有环,求无向图的连通分量个数等。比如典型应用:实现Kruskar算法求最小生成树。
下面举例说明并查集的常用的三种操作:
1、make_set() 把每一个元素初始化为一个集合
初始化后每一个元素的父亲节点是它本身,每一个元素的祖先节点也是它本身(也可以根据情况而变)。
2、find_set(x) 查找一个元素所在的集合
查找一个元素所在的集合,其精髓是找到这个元素所在集合的祖先!这个才是并查集判断和合并的最终依据。
判断两个元素是否属于同一集合,只要看他们所在集合的祖先是否相同即可。
合并两个集合,也是使一个集合的祖先成为另一个集合的祖先.(祖先节点即下标与其值相等 即 father[i]=i)
3、merge_set(x,y) 合并x,y所在的两个集合
合并两个不相交集合操作很简单:
利用find_set找到其中两个集合的祖先,将一个集合的祖先指向另一个集合的祖先。
其中2中写并查集时涉及到的路径压缩,最好用递归,一方面代码的可读性非常好,另一方面,可以更直观的理解路径压缩时在回溯时完成的巧妙。
下面以HDOJ上的题目为例编程实现。
hdoj 1232
http://acm.hdu.edu.cn/showproblem.php?pid=1232
畅通工程,实际上就是求这个图的连通分量的问题,如果有 n 个,那么最少还需要 n-1 根线将它们连接起来。求连通分量的话,
可以使用深度优先或是广度优先遍历的方法,遍历一遍,有多少个起点,就是有多少个连通分量;
也可以用并查集的思路,连通的节点放在在一起(即有同一个祖先节点),最后数一数有多少“集合”就 o好了。并查集 不用占很多
内存空间,理论上高效些。最后 连通量个数-1就是要修的最少的路了。
#include
#include
#define N 1001
int father[N];//* father[x]表示x的父节点*/
int mark[N];//* mark[x]表示x是否在集合中*/
void make_set()//1初始化
{
int i;
for(i=1; i
http://acm.hdu.edu.cn/showproblem.php?pid=1272
根据题意,只要判断下面两个条件即可。
1. 判断是否成环(回路);
2. 判断是否只有一棵树(只有一个连通图);
这个题目 利用并查集可以容易解决,要是利用图论的方法,那就麻烦很多了。
下面是实现代码:
#include
#include
#define M 100001
int father[M];/* father[x]表示x的父节点*/
int vis[M]; // vis[x] 表示节点x是否在集合中
void make_set()//1初始化
{
memset(vis, 0, sizeof(vis));
int i;
for(i=1; i1)//多个连通分支
{
flag = 0;
break;
}
}
}
if(flag)
printf("Yes\n");
else
printf("No\n");
}
return 0;
}
上面的int find_set(int x) 查找父节点采用的是一采用递归的方式压缩路径, 但是,递归压缩路径可能会造成溢出栈,或者对于求带环路的(如欧拉回路易出问题),下面我们说一下非递归方式进行的路径压缩:
int find_s(int x)
{
int k, j, r;
r = x;
while(r != father[r]) //查找跟节点
r = father[r]; //找到跟节点,用r记录下
k = x;
while(k != r) //非递归路径压缩操作
{
j = father[k]; //用j暂存parent[k]的父节点
father[k] = r; //parent[x]指向跟节点
k = j; //k移到父节点
}
return r; //返回根节点的值
}
此外并查集的题目 经典的还有并查集,已有牛人分析,参见 ,代码参见2。
参考资料:
http://zh.wikipedia.org/zh-cn/%E5%B9%B6%E6%9F%A5%E9%9B%86
《算法导论》