并查集以及路径压缩
一、概念
并查集是一种树型的数据结构,他的思想是用一个数组表示了整片森林(parent),树的根节点唯一标识了一个集合,我们只要找到了某个元素的的树根,就能确定它在哪个集合里。他有两个操作:
- 合并(Union):把两个不相交的集合合并为一个集合。
- 查询(Find):查询两个元素是否在同一个集合中。这个确定方法就是不断向上查找找到它的根节点,它可以被用来确定两个元素是否属于同一子集。
二、操作
每一个集合用一棵树来表示。树根的编号就是整个集合的编号。每个节点存储它的父节点, parents[x] 表示 x 的父节点。
2.1 初始化
并查集的重要思想在于,用集合中的一个元素代表集合。因为刚开始每个集合只有一个点,所以直接让本身作为父节点即可。parents[x] = x 是祖宗节点的一个标志,因为只要不是祖宗节点,parents[x] 存储的是 x 的父节点。
那么当前的parents数组就应该是
i | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
parents[i] | 1 | 2 | 3 | 4 | 5 |
2.2 合并
当我们想让一个节点的集合加入另一个节点的集合时,使用合并操作,修改他们parents数组的值即可,例如我们想让2、3合并以及4、5合并,那么修改parents[3] = 2, parents[5] = 4
i | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
parents[i] | 1 | 2 | 2 | 4 | 4 |
而此时我们希望4的集合再加入2的集合里,那么同理修改parents[4] = 2
i | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
parents[i] | 1 | 2 | 2 | 2 | 4 |
2.3 查找
这个方法的是利用parents数组,不断向上寻找x的祖先,直到找到一个节点的parents就是他自己,即为该节点的最祖先节点。他的伪代码是这样的
while(parents[该节点]!=该节点)
{
该节点 = parents[该节点]
}
例如拿上图来说,希望找到节点5的最祖先节点:
1. 找到parents[5] = 4
2. 找到parents[4] = 2
3. 发现parents[2] = 2,那么返回节点2
2.4 路径压缩
上述的方法是并查集树林的最基础的表示方法,这个方法不会比链表法好,这是因为创建的树可能会严重不平衡,因此可以用路径压缩的方法优化。
路径压缩是一种在执行“查找”时扁平化树结构的方法。关键在于在路径上的每个节点都可以直接连接到根上;他们都有同样的表示方法。为了达到这样的效果,Find查找节点时,会依次改变该节点的所有祖先节点的引用到根节点。得到的树将更加扁平,为以后直接或者间接引用节点的操作加速。
三、代码实现
vector<int> parents;//并查集,存储每个节点的父节点/祖先节点
//初始化并查集数组,初始时令所有的节点的父节点都是自己
void initialparent(int nodenum)
{
parents.resize(nodenum, -1);
for (int i = 0; i < nodenum; i++)
{
parents[i] = i;
}
}
//寻找node节点的最祖先的节点,同时对该节点的所有祖先做路径压缩
int find(int node)
{
//寻找最祖先的节点
int r = node;
while (parents[r] != r)
{
r = parents[r];
}
int t = node;
//压缩路径
while (t != r)//t是从该节点node开始向上到达祖先节点r的所有节点
{
int temp = parents[t];
parents[t] = r;
t = temp;
}
return r;
}
//合并两个节点的集合
void merge(int x,int y)
{
int r1 = find(x);
int r2 = find(y);
if (r1 == r2)
return;
if (parents[r1] > parents[r2]) {
parents[r1] = r2;
}
else {
parents[r2] = r1;
}
}