在一些有N个元素的几何应用问题中,通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的几何合并,其间反复查找一个元素在哪个集合中。这类题目看似并不复杂,但数据量极大,若用正常的数据结构来描述的话,往往在空间上消耗过大,即使在空间上勉强通过,时间复杂度也是极高的,所以这类问题应该用并查集类解决。
并查集类似一个森林,每个节点均有一个fater[x]来表示他的父节点。
1. 并查集的初始化:Make_Set(int x)
void Make_Set(int x)//要根据实际情况指定父节点,此处初始化本身为根节点
{
father[x] = x;
}
2.查找:Find_Set(int x)
例:想知道元素3所在的几何。通过father[3] = 2; father[2] = 1; father[1] = 1;
, 即确定了元素3所在的集合为1。每次查找元素x所在的集合所需要的运算次数等于元素x所在的深度。
这里的查找有个路径压缩的优化,即得到3所在集合为1时,可以直接赋值father[3] = 1;
,而且在查找3时会查找2,可能还会查找其他的元素?,将这些元素都赋值father[?] = 1;
,这样在下一次查找3的子孙所在的集合时,查找的次数都缩短了1;在下一次查找3的祖先所在的集合时,查找的次数都变为1了。
int Find_Set(int x)//回溯时即为路径压缩
{
if (x != father[x])
{
father[x] = Find_Set(father[x]);
}
return father[x];
}
//return father[x] = (x != father[x]) ? Find_Set(father[x]) : x;
3. 合并:Union(int x, int y)
即合并x和y所在的两个集合,只需要把其中一个集合的根节点设置为另一个集合根节点的孩子即可。例如现在有3个节点。合并1和2,可以是这两种情况,效果一样,但是再和3合并,这两种情况就不同了,在查找元素3所在的集合的时候,前者先找到father[3] = 2,再找到father[2] = 1,两次找到3所在的集合为1,而后者只需要一次查找。这里后者优于前者,所以在合并时有一个启发式合并的优化,即记录每个集合的深度,当要合并两个集合时,将深度较小的集合挂在深度大的集合下面。
void Union(int x, int y)
{
int fx = Find_Set(x);
int fy = Find_Set(y);
if (fx == fy)
return ;
if (rank[fx] > rank[fy])
{
father[fy] = fx;
if (rank[fy] + 1 > rank[fx])
rank[fx] = rank[fy] + 1;
}
else
{
father[fx] = fy;
if (rank[fx] + 1 > rank[fy])
rank[fy] = rank[fx] + 1;
}
}
上面的这个用来启发式合并的秩可以自己改变,总之要使问题能优化,这个秩也可设为集合中元素的个数。需要说明的一点,这个秩,仅仅只有根节点的rank[]值保存的是正确的信息,rank[x]并不等于rank[fx],而且可以通过查找得到x所在集合的根节点fx,所以rank[x]不是必须的(此优化效果不是很明显,作用不大,有时这样分情况讨论麻烦,也可直接将y挂到x上)
4. 总结:
并查集适用与所有几何的合并与查找操作,进一步还可以延伸到一些图论中判断两个元素是否属于同一个连通块时的操作。由于使用启发式合并和路径压缩,可以将并查集的时间复杂度近似的看作O(1),空间复杂度近似看作O(n),这样就将一个大规模的问题转变成空间极小、速度极快的简单操作。