从树到并查集,再到最小生成树(详细)

引入

直观地说,并查集是一种合并不同树的方法。什么?你不知道树是啥?好吧,这里说点题外话不看直接跳过到并查集部分),每回我要学什么东西的时候(记为知识点A),又会发现很多前置知识(知识点B、C等)没有学,而去搜那些前置知识,又只学得到一些对解决我现在要学的东西无关紧要的一些知识,虽然说总得学,但总觉得有那么一点不爽,看B、C半天也没看出什么名堂,还是得靠学好A来反过来加深对B、C的理解。所以!为了看的人更爽(虽然或许没什么人看),我首先介绍树,并在之后使用各种专有名词的时候直接加上注解,希望能有所帮助。

树是一种数据结构,表示每个点之间的关系。例如,下图是一种常见的树:树 - Example

树包含节点和边。图中各个圆圈就是一个个节点,而把节点连起来线的叫做边。

树有几个重要特点:

  1. 不含环。环可以简单理解为死循环。树是图的特殊情况。而图则是可以含有环的树。没有环也就意味着,从树上随便选一节点,沿着路径一直走,如果不走回头路,就不可能回到原来经过过的节点。
  2. 恰好包含n - 1条边。
  3. 连通。连通是指对树上任何两个节点A,B,总有一条路径能从A走到B。

除开这些性质和结构外,树还有一个根节点。根节点由人确定,与根节点直接链接的节点成为根的子节点,然后这些子节点作为子根,再往下链接又可以确定很多子树……例如上图,A是整个树的根节点,而B和C是它的子节点(child),相应的,A是B、C节点的父节点(parent)。此时,把B,D,G,H,I单独拿出来,也是一个完整的树,我们称其为子树,B是子树的根节点。

好了,介绍完树的性质,我们想它的实现。如何在代码中实现树?首先,我们需要定义各个节点,其次,我们需要将各个节点联系起来,形成一棵树。

这里提供一个示例:

//首先建立节点结构体,里面有节点要存储的信息。
struct node{
	int x;//例如,节点存某个数字.
}Node[1000];//将1000个节点存到node数组里

将节点联系起来,我们可以使用两种方法:
一是邻接矩阵,我们给一个矩阵,a[i][j]表示第i个和第j个节点之间有一条边。例如对于下面的树:
在这里插入图片描述
其邻接矩阵表示为:

节点序号123456789
1truetruetruefalsefalsefalsefalsefalsefalse
2truetruefalsetruefalsefalsefalsefalsefalse
3truefalsetruefalsetruetruefalsefalsefalse
4falsetruefalsetruefalsefalsefalsefalsefalse
5falsefalsetruefalsetruefalsefalsetruetrue
6falsefalsetruefalsefalsetruefalsefalsefalse
7falsefalsefalsetruefalsefalsetruefalsefalse
8falsefalsefalsefalsetruefalsefalsetruefalse
9falsefalsefalsefalsetruefalsefalsefalsefalse

如上表,用邻接矩阵可以表示任意两个节点是否直接相连。
如果只有这个表,我们无法确定树的根节点。事实上,树的根节点本也是人为规定的,不影响树的形状。

事实上,n - 1个边信息可以由邻接矩阵确定~

另外还有一种联系方法,就是描述出这n - 1条边,每个边都有一个左端点和右端点。

struct edge//edge是边的英文
{
	node* left, right;
	//这里用node指针只是一种方法,如果节点用数组存储,也可以用整形表示下标。
};

之后,一个一个输入边的信息即可。

那么树的性质就介绍到这里。

并查集

实现

在关于树的问题中,我们或许会遇到有关树的连通性的问题。

例如,给若干个边的数据(左右节点),要你检验,这些边和节点是否构成一棵完整的树。

在并查集算法中,我们可以存储每个节点的前导节点(也就是谁与它相连),其利用结构体可以实现,利用数组也可以实现。而为了方便以下标做统计合并树,我们不妨使用数组:int pre[1001]表示1~1000这些节点的父节点。

而如何将他们连通?

例如,现在有节点1,2,3,4,我们给出2条边的数据,让每行表示一条边的左端点和右端点:

1 3
2 4

于是我们的任务是:将属于一个边的节点加入一个连通块(连通块即一个子树)。而做到这点就要保证它们的根节点一定是一样的。
不妨定义两个函数:find函数和join函数。一个用于查询根节点,一个在根节点不同时将第二个节点的根节点b与第一个节点的根节点a相连,并指定b的根节点是a,这样就合并了树,连接了边。当然,根节点的父节点是它自己。

int find(int x)//输入下标,返回其根节点的下标
{
	while(pre[x] != x)
		x = pre[x];
	return x;
}

void join(int x, int y)//判断二者是否由同一个根节点衍生,否则合并
{
	int rootX = find(x), rootY = find(y);
	if(rootX != rootY){
		pre[rootY] = rootX;
	}
}

初始的时候,所有的节点都没有连通,每个节点都在一个由自己构成的单点树,因此树根是它自己。所以设pre[i] = i.

将这些代码组合在一起,形成并查集的基本逻辑(假设有编号为1~m的m个节点,以及n条边):

	int pre[m];
	for(int i = 0; i < m; ++i) pre[i] = i;//初始化
	int right[n], left[n];
	for(int i = 0; i < n; ++i) cin >> right[i] >> left[i];//读取边数据

	for(int i = 0; i < n; ++i)
		join(right[i], left[i]);

由此,我们就实现了并查集!就是这么简单^ _ ^

例题

HDU1232 - 畅通工程

畅通工程的裸题,做完这题你就会并查集辣~

最小生成树

“村村通"问题

假设现在有一个村落,里面有很多村庄,各个村庄之间有的可以修路,有的因为地形原因无法修路。而各个可以修的路也都有各自的预计开销。现在要你设计一个方案,使得各个村庄之间连通起来(即各个村庄之间直接或者间接连接),并保证你的方案是预算最小的。

分析
实际上,一个树/图的边如果有一个权值(例如这里的修路开销)时,我们就称其为带权树/图。这里的各个村庄之间可以修的路和这些村庄就构成了一个带权图。另外,这里的各个村庄之间的路是可以来回走的,因此它是一个无向带权图。

问题可抽象化为:

给定一个无向带权图,求一个权值和最小的子图,使得各个节点都在这个子图中。

以贪心的思想,不难发现,要使得权值和最小,就利用最少的边构成这个子图。而构成一个子图需要的最少边是n - 1个,这个时候构成的就是一个树。我们称这个树为这个带权图的最小生成树。

下面我们来详细介绍最小生成树和求出它的算法。

概念

首先来看生成树的概念:

生成树:

一个有 n 个结点的连通图的生成树是原图的极小连通子图,且包含原图中的所有 n 个结点,并且有保持图连通的最少的边。

对于一个边大于n-1条的图,显然最小生成树不止有一个。

接着是最小生成树:

首先要有一个条件:图必须是带权图,不然各个生成树就没有比较的意义了,也就不存在最小生成树一说。

所有生成树中,权值和最小的那一棵就是最小生成树。

有些时候,最小生成树也不唯一。

Kruskal算法

Kruskal是利用边贪心思想求最小生成树的方法。Kruskal求最小生成树需要用到并查集合并子树。

首先将所有的边按照权值排序。
接下来,依次取出这些边。

分别查看取出边的左右节点的父节点。
如果不同的话,说明这两个节点目前并未连通,则利用并查集算法将它们合并。
如果相同的话,说明这两个节点已经连通,这个时候这条边就多余了,就可以不用连了。

按以上方法处理至连接n - 1次,我们的最小生成树就连好了。

伪代码:

sort edge

int count = 0
int ans = 0
for i from 1 to sizeOfEdge
	if (edge[i].leftPoint.father != edge[i].rightPoint.father)
	{
		edge[i].rightPoint.father.father = edge[i].leftPoint.father
		count = count + 1
		ans = ans + edge[i].value
	}
	end if
end for

return ans

概括来说,就是先把边排序,再贪心取可以取的小边,并把小边所在的端点合并即可。

练习题:

HDU1233 - 还是畅通工程

Prim算法

未完待续

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值