一口气——并查集及其在Kruskal算法的应用

本文记录下树结构下的并查集和其在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. 第一条线索1、2,以左为尊即2组此时归为1组,2号的组长为1号(1 1 3 4 5 6 7 8 9 10 11)
  2. 第二条线索3、4,同上以左为尊4组归为3组,4号的组长为3号(1 1 3 3 5 6 7 8 9 10 11)
  3. 第三条线索5、2,注意以左为尊不是编号越小越尊贵而是输入的顺序,这里需要让2号所在的组归为5号所在的组,2号在1组,其组长为1号,贯彻一撸到底,让1号带着2号归为5号所在的5组(5 1 3 3 5 6 7 8 9 10 11)
  4. 第四条线索4、6,6组归为4号所在的3组(5 1 3 3 5 3 7 8 9 10 11)
  5. 第五条线索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)
  6. 第六条线索7、11,以左为尊(5 5 5 3 5 3 7 8 9 10 7)
  7. 第七条线索8、7,以作为尊(5 5 5 3 5 3 8 8 9 10 7)
  8. 第八条线索9、7,一撸到底后以左为尊(5 5 5 3 5 3 8 9 9 10 7)
  9. 第九条线索9、11,一撸到底过程中路径压缩,11号先找到7号,而7号也不能一步到达自己的组长9号,7号到组长9号间的8号被赶跑(5 5 5 3 5 3 9 9 9 10 9)
  10. 第十条线索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;
}

猜你喜欢

转载自blog.csdn.net/ccmtvv/article/details/106560333
今日推荐