并查集:(union-findsets)
一种简单的用途广泛的集合. 并查集是若干个不相交集合,能够实现较快的合并和判断元素所在集合的操作,应用很多,如其求无向图的连通分量个数等。最完美的应用当属:实现Kruskar算法求最小生成树。
并查集的精髓(即它的三种操作,结合实现代码模板进行理解):
1、Make_Set(x) 把每一个元素初始化为一个集合
初始化后每一个元素的父亲节点是它本身,每一个元素的祖先节点也是它本身(也可以根据情况而变)。
/* 初始化集合*/
void Make_Set(int x)
{
father[x] = x; //根据实际情况指定的父节点可变化
rank[x] = 0; //根据实际情况初始化秩也有所变化
}
2、Find_Set(x) 查找一个元素所在的集合
查找一个元素所在的集合,其精髓是找到这个元素所在集合的祖先!这个才是并查集判断和合并的最终依据。
判断两个元素是否属于同一集合,只要看他们所在集合的祖先是否相同即可。
合并两个集合,也是使一个集合的祖先成为另一个集合的祖先。
优化一:
使用并查集查找时,如果查找次数很多,那么使用朴素版的查找方式肯定要超时。比如,有一百万个元素,每次都从第一百万个开始找,这样一次运算就是10^6,如果程序要求查找个一千万次,这样下来就是10^13,肯定要出问题的。
这是朴素查找的代码,适合数据量不大的情况:
int findx(int x)
{
int r=x;
while(parent[r] !=r)
r=parent[r];
return r;
}
下面是采用路径压缩的方法查找元素:所谓路径压缩,就是查找的同时,顺路把该经过的元素都直接指向父节点,方便下次查找!这样以后再次查找时复杂度就变为O(1)
int find(int x) //查找x元素所在的集合,回溯时压缩路径
{
if (x != parent[x])
{
parent[x] = find(parent[x]); //回溯时的压缩路径
} //从x结点搜索到祖先结点所经过的结点都指向该祖先结点
return parent[x];
}
上面是一采用递归的方式压缩路径, 但是,递归压缩路径可能会造成溢出栈,我曾经因为这个RE了n次,下面我们说一下非递归方式进行的路径压缩:
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; //返回根节点的值
}
3、Union(x,y) 合并x,y所在的两个集合
合并两个不相交集合操作很简单:利用Find_Set找到其中两个集合的祖先,将一个集合的祖先指向另一个集合的祖先。如图
如图.每个儿子都指向最顶的祖先,这样查询就变为O(1)
优化二:
Union(x,y)时 按秩(高度)合并
即合并的时候将高度少的集合合并到元素多的集合中,这样合并之后树的高度会相对较小。
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;
}
}
送上几道POJ好题:
http://162.105.81.212/JudgeOnline/problem?id=1703
http://162.105.81.212/JudgeOnline/problem?id=2421
http://162.105.81.212/JudgeOnline/problem?id=2492
http://162.105.81.212/JudgeOnline/problem?id=1861
http://162.105.81.212/JudgeOnline/problem?id=1308
http://162.105.81.212/JudgeOnline/problem?id=2524
部分转自http://www.cnblogs.com/vongang/