1. 概论
定义:
并查集是一种树型的数据结构,用于处理一些不相交集合的合并及查询问题(即所谓的并、查)。比如说,我们可以用并查集来判断一个森林中有几棵树、某个节点是否属于某棵树等。
主要构成:
并查集主要由一个整型数组pre[ ]和两个函数find( )、join( )构成。
数组 pre[ ] 记录了每个点的父亲节点是谁,函数 find(x) 用于查找指定节点 x 属于哪个集合,函数 join(x,y) 用于合并两个集合 x 和 y 。
作用:
并查集的主要作用是求连通分支数(如果一个图中所有点都存在可达关系(直接或间接相连),则此图的连通分支数为1;如果此图有两大子图各自全部可达,则此图的连通分支数为2……)
find( )函数的定义与实现
在并查集中,用于查询各自的根节点的函数就是我们的find()函数。find(x)的作用是用于查找某个给定的点x,返回其所属集合的代表。
实现:
首先我们需要定义一个数组:int pre[1000]; (数组长度依题意而定)。这个数组记录了每个人的父节点是谁。这些人从0或1开始编号(依题意而定)。比如说pre[16]=6就表示16号的父节点是6号。如果一个人的父节点就是他自己,那说明他就是根节点了,查找到此结束。
每个人都只认自己的父节点。要想知道自己根节点的名称,只能一级级查上去。因此你可以视find(x)这个函数就是根节点用的。
下面给出这个函数的具体实现
int find(int x) //查找 x 的根节点
{
while(pre[x] != x) //如果 x 的父节点不是自己(则说明找到的点不是根节点)
x = pre[x]; // x 继续找他的上级,直到找到根节点为止
return x; //返回根节点
}
join( )函数的定义与实现
用于合并两个节点 x 和 y
实现:
join(x,y)的执行逻辑如下:
1、寻找 x 的根节点;
2、寻找 y 的根节点;
3、如果 x 和 y 不相等,则随便选一个点作为另一个点的父节点,如此一来就完成了 x 和 y 的合并
void join(int x,int y) //让 x 节点和 y 节点合并
{
int fx=find(x), fy=find(y); //寻找这两个点的根节点
if(fx != fy) //如果两个根节点不相同
pre[fx]=fy; //就随意选一个当父节点,另一个作为子节点
}
路径压缩算法之一(优化find( )函数)
问题引入:
前面介绍的 join(x,y) 实际上为我们提供了一个将不同节点进行合并的方法。通常情况下,我们可以结合着循环来将给定的大量数据合并成为若干个更大的集合(即并查集)。但是问题也随之产生,我们来看这段代码:
if(fx != fy)
pre[fx]=fy;
这里并没有明确谁是谁父节点的的规则,而是我直接指定后面的数据作为前面数据的父节点。那么这样就导致了最终的树状结构无法预计,即有可能是良好的 n 叉树,也有可能是单支树结构(一字长蛇形)。试想,如果最后真的形成单支树结构,那么它的效率就会及其低下(树的深度过深,那么查询过程就必然耗时)。
而我们最理想的情况就是所有人的直接父节点都是根节点,这样一来整个树的结构就只有两级,此时查询根节点只需要一次。因此,这就产生了路径压缩算法。
实现:
从上面的查询过程中不难看出,当从某个节点出发去寻找它的根节点时,我们会途径一系列的节点,在这些节点中,除了根节点外,其余所有节点,都将父节点更改为根节点。
因此,基于这样的思路,我们可以通过递归的方法来逐层修改返回时的某个节点的父节点(即pre[x]的值)。简单说来就是将 x 到根节点路径上的所有点的pre(上级)都设为根节点。下面给出具体的实现代码:
int find(int x) //查找结点 x 的根结点
{
if(pre[x] == x) return x; //递归出口: x 的上级为 x 本身,即 x 为根结点
pre[x] = find(pre[x]) //此代码相当于先找到根结点 rootx,然后pre[x]=rootx
return pre[x];
}
该算法存在一个缺陷:只有当查找了某个节点的父节点后,才能对该查找路径上的各节点进行路径压缩。换言之,第一次执行查找操作的时候是实现没有压缩效果的,只有在之后才有效。
路径压缩算法之二(加权标记法)
主要思路:
加权标记法需要将树中所有节点都增设一个权值,用以表示该节点所在树中的高度(比如用rank[x]=3表示 x 节点所在树的高度为3)。这样一来,在合并操作的时候就能通过这个权值的大小来决定谁当谁的父节点
在合并操作的时候,假设需要合并的两个集合的更节点分别为 x 和 y,则只需要令pre[x] = y 或者pre[y] = x 即可。但我们为了使合并后的树不产生退化(即:使树中左右子树的深度差尽可能小),那么对于每一个元素 x ,增设一个rank[x]数组,用以表达子树 x 的高度。在合并时,如果rank[x] < rank[y],则令pre[x] = y;否则令pre[y] = x。
举个例子,我们对以A,F为代表元的集合进行合并操作(如下图所示)
由于rank(A) > rank(F) ,因此令pre[F]= A。合并后的图形如下图所示:
可以看出,合并前两个树的最大高度为3,合并后依然是3,这也就达到了我们的目的。但如果令pre[A]= F,那么就会使得合并后的树的总高度增加,这里我就不上图了,同学们不信可以自己画出来看看。
实现:
加权标记法的核心在于对rank数组的逻辑控制,其主要的情况有:
1、如果rank[x] < rank[y],则令pre[x] = y;
2、如果rank[x] == rank[y],则可任意指定上级;
3、如果rank[x] > rank[y],则令pre[y] = x;
代码:
void union(int x,int y)
{
x=find(x); 寻找 x 的代表元
y=find(y); 寻找 y 的代表元
if(x==y) return ; 如果 x 和 y 的代表元一致,
说明他们共属同一集合,则不需要合并直接返回;
否则,执行下面的逻辑
if(rank[x]>rank[y]) pre[y]=x; 如果 x 的高度大于 y,则令 y的父节点为 x
else 否则
{
if(rank[x]==rank[y]) rank[y]++; 如果 x 的高度和 y 的高度相同,则令 y 的高度加1
pre[x]=y; 让 x的上级为 y
}
}
下面给出上述所有内容的代码汇总:
const int N=1005 指定并查集所能包含元素的个数(由题意决定)
int pre[N]; 存储每个结点的前驱结点
int rank[N]; 树的高度
void init(int n) 初始化函数,对录入的 n 个结点进行初始化
{
for(int i = 0; i < n; i++){
pre[i] = i; 每个结点的上级都是自己
rank[i] = 1; 每个结点构成的树的高度为 1
}
}
int find(int x) 查找结点 x的根结点
{
if(pre[x] == x) return x; 递归出口:x 的上级为 x 本身,则 x 为根结点
return find(pre[x]); 递归查找
}
int find(int x) 改进查找算法:完成路径压缩,
将 x的上级直接变为根结点,那么树的高度就会大大降低
{
if(pre[x] == x)
return x; //递归出口:x的上级为 x本身,即 x为根结点
return pre[x] = find(pre[x]); 此代码相当于先找到根结点 rootx,然后 pre[x]=rootx
}
bool isSame(int x, int y) 判断两个结点是否连通
{
return find(x) == find(y); 判断两个结点的根结点(即代表元)是否相同
}
bool join(int x,int y)
{
x = find(x); 寻找 x的代表元
y = find(y); 寻找 y的代表元
if(x == y)
return false; 如果 x和 y的代表元一致,说明他们共属同一集合,
则不需要合并,返回 false,表示合并失败;
否则,执行下面的逻辑
if(rank[x] > rank[y])
pre[y]=x; 如果 x的高度大于 y,则令 y的上级为 x
else 否则
{
if(rank[x]==rank[y])
rank[y]++; 如果 x的高度和 y的高度相同,则令 y的高度加1
pre[x]=y; 让 x的上级为 y
}
return true; 返回 true,表示合并成功
}
练习
【蓝桥杯】 历届试题 合根植物(并查集)_给定 m \times nm×n 个小格子(东西方向 mm 行,南北方向 nn 列)。每个格子里种了-CSDN博客
【蓝桥杯】 历届试题 国王的烦恼(并查集)_c国由n个小岛组成,为了方便小岛之间联络,c国在小岛间建立了m座大桥,每座大桥连接-CSDN博客