本文记录下树结构下的并查集和其在Kruskal计算最小生成树算法中的应用
一、何为并查集
并查集,顾名思义对数据进行合并和查询,因为是树结构的应用,合并即将两个数据安置在树中,查询即查询某个数据的祖宗结点。其意义在于将许多看似不相关的数据通过一些线索分组,下面举个例子。
心理学中有个著名的六度分离理论,“你和任何一个陌生人之间所间隔的人不会超过五个,也就是说,最多通过五个人你就能够认识任何一个陌生人。”
现有11个人,这11个人编号1~14的数据,有如下10条线索:1、2彼此认识;3、4彼此认识;5、2彼此认识;4、6彼此认识;2、6彼此认识;7、11彼此认识;8、7彼此认识;9、7彼此认识;9、11彼此认识;1、6彼此认识。那么我们想要认识这11个人,只需要认识他们之中的几个,通过这几个人便可以要到所有人的联系方式,利用并查集将这11个人根据线索分组。
二、如何合并、查询
我认为有两个原则,一个概念,以左为尊原则和一撸到底原则,同时在一撸到底的过程中会伴随路径压缩的概念。以左为尊指让右方所在的组归为左方所在的组,一撸到底指分组时要让自己组长以左为尊,具体是什么通过分析上面的例子看一下。
起初每人自成一组共11组并自认组长(1 2 3 4 5 6 7 8 9 10 11)
- 第一条线索1、2,以左为尊即2组此时归为1组,2号的组长为1号(1 1 3 4 5 6 7 8 9 10 11)
- 第二条线索3、4,同上以左为尊4组归为3组,4号的组长为3号(1 1 3 3 5 6 7 8 9 10 11)
- 第三条线索5、2,注意以左为尊不是编号越小越尊贵而是输入的顺序,这里需要让2号所在的组归为5号所在的组,2号在1组,其组长为1号,贯彻一撸到底,让1号带着2号归为5号所在的5组(5 1 3 3 5 6 7 8 9 10 11)
- 第四条线索4、6,6组归为4号所在的3组(5 1 3 3 5 3 7 8 9 10 11)
- 第五条线索2、6,此时6号的组长为3号;2号的组长为1号、1号的组长又为5号,即一撸到底后2号组长为5号,在这个过程中发生了路径压缩,2号到组长5号的距离中间横着的1号被赶跑,也就是现在2号想要找到组长不需要在通过1号了。再将6号的所在的组以左为尊(5 5 5 3 5 3 7 8 9 10 11)
- 第六条线索7、11,以左为尊(5 5 5 3 5 3 7 8 9 10 7)
- 第七条线索8、7,以作为尊(5 5 5 3 5 3 8 8 9 10 7)
- 第八条线索9、7,一撸到底后以左为尊(5 5 5 3 5 3 8 9 9 10 7)
- 第九条线索9、11,一撸到底过程中路径压缩,11号先找到7号,而7号也不能一步到达自己的组长9号,7号到组长9号间的8号被赶跑(5 5 5 3 5 3 9 9 9 10 9)
- 第十条线索1、6,一撸到底过程中路径压缩,6号到组长5号中间的3号被赶跑(5 5 5 3 5 5 9 9 9 10 9)
线索/人 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
---|---|---|---|---|---|---|---|---|---|---|---|
起初 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
1、2 | 1 | 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
3、4 | 1 | 1 | 3 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
5、2 | 5 | 1 | 3 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
4、6 | 5 | 1 | 3 | 3 | 5 | 3 | 7 | 8 | 9 | 10 | 11 |
2、6 | 5 | 5 | 5 | 3 | 5 | 3 | 7 | 8 | 9 | 10 | 11 |
7、11 | 5 | 5 | 5 | 3 | 5 | 3 | 7 | 8 | 9 | 10 | 7 |
8、7 | 5 | 5 | 5 | 3 | 5 | 3 | 8 | 8 | 9 | 10 | 7 |
9、7 | 5 | 5 | 5 | 3 | 5 | 3 | 8 | 9 | 9 | 10 | 7 |
9、11 | 5 | 5 | 5 | 3 | 5 | 3 | 9 | 9 | 9 | 10 | 9 |
1、6 | 5 | 5 | 5 | 3 | 5 | 3 | 9 | 9 | 9 | 10 | 9 |
总结下来,合并就是将两个结点安插在树中,安插前就需要查询左边点和右边点的祖宗结点(一撸到底),在查询过程中可能有的点会发现自己以为的祖宗结点其实是父结点,这样的点会找到自己真正的祖宗结点(路径压缩),最后让左边的点跟随右边的点(以左为尊)。以上例子中的组长便是祖宗结点,起初每个点的祖宗结点就是自己,并查集实现了结点们认祖归宗的过程,最后有几个祖宗结点,数据就分为了几组。该例为3个祖宗,分别是5、9、10,即通过这三个人就可以认识所有的11个人。
#include <iostream>
#include <algorithm>
#include <stdio.h>
using namespace std;
int point_data[101];//索引为自己的结点号,值为父结点号(可能是祖宗节点)
int n, m;//结点数,线索数
int query(int v) {//寻找祖宗结点,查询
if (point_data[v] == v)
return v;
else {
point_data[v] = query(point_data[v]);//一撸到底原则,伴随路径压缩
return point_data[v];
}
}
void merge(int left, int right) {//两个结点合并
int t1 = query(left);//获得left祖宗结点
int t2 = query(right);//获得right祖宗结点
if (t1 != t2)
point_data[t2] = t1;//以左为尊原则
return;
}
int main() {
cout << "输入结点数和相关信息数:";
cin >> n >> m;//有多少结点,多少个相关性信息
for (int i = 1; i <= n; i++)
point_data[i] = i;
cout << "输入相关的结点" << endl;
for (int i = 1; i <= m; i++) {
int x, y;//x和y相关
cin >> x >> y;
merge(x, y);//合并x和y
}
cout << "祖宗结点有:";
for (int i = 1; i <= n; i++)
if (point_data[i] == i)//该点一定为一个祖宗结点
cout << i << " ";
cout << endl;
return 0;
}
三、并查集在Kruskal最小生成树算法中的应用
并查集可以查询某两个结点的祖宗结点,如果两个结点的祖宗结点一样,且这两个结点间有一条边,则从树结构变成了有闭合回路的图。相信已经很明显了,并查集可以判断在图中加入一条边后是否形成环,而这正是Kruskal最小生成树算法的关键。
大致说下该宗室级算法,首先将图中每条边去除并按权值自然顺序排序,然后依次将边补充回图中,如果形成了回路则该边舍弃,循环该步骤直到所有点被边连接。
#include <iostream>
#include <stdio.h>
#include <algorithm>
using namespace std;
struct Line {//每条边
int p1;//点
int p2;//点
int weight;//权值
};
int point_data[101];//并查集数组
int query(int i) {//查询,寻找祖宗结点
if (point_data[i] == i)
return point_data[i];
else {
point_data[i] = query(point_data[i]);//路径压缩
return point_data[i];
}
}
bool merge(int left, int right) {//合并
int t1 = query(left);//获得祖先结点
int t2 = query(right);//获得祖先结点
if (t1 != t2) {//如果没有共同的祖先结点,则无回路
point_data[t2] = t1;
return true;
}
return false;
}
int main() {
int n, m;
cout << "输入点、边数:";
cin >> n >> m;
Line lines[101];
cout << "输入边的信息" << endl;
for (int i = 1; i <= m; i++)
cin >> lines[i].p1 >> lines[i].p2 >> lines[i].weight;
auto cmp = [](const Line& a, const Line& b)->int {return a.weight < b.weight; };
sort(lines + 1, lines + 1 + m, cmp);//按照权值排序
for (int i = 1; i <= n; i++)//并查集初始化
point_data[i] = i;
int count = 0, sum = 0;//已用边数,当前总权值
cout << "所用边权值依次为:";
for (int i = 1; i <= m; i++) {
if (merge(lines[i].p1, lines[i].p2)) {//如果不会形成回路
cout << lines[i].weight << " ";
count++;//已用边数
sum += lines[i].weight;
}
if (count == n - 1)//n-1条边恰好可将n个点相连
break;
}
cout << endl << "总权值为:" << sum << endl;
return 0;
}