生成树
基本概念
一个连通图的生成树是一个极小连通子图,其含有图中的全部顶点,和构成一颗树的(n-1)条边。
所以生成树的要求是:
连通图的连通子图
n个顶点
n-1个边
==图的所有生成树中具有边上的权值之和最小的树称为图的最小生成树==
所以构造最小生成树的准则:
- 必须只使用该图中的边来构造最小生成树
- 必须使用且仅只使用(n-1)条边来连接图中的n个顶点
- 不能使用产生回路的边
城市之间的交通工程造价最优问题,就是典型的最小生成树应用。
求图的最小生成树的两个算法:
普利姆(Prim)算法
- 朴素版Prim算法——适用于稠密图 $O(n^2)$——适合用邻接矩阵实现,用链式向前星很麻烦
- 堆优化版Prim算法 ——一般不常用$O(mlogn)$
克鲁斯卡尔(Kruskal)算法——使用与稀疏图 $O(mlogm)$
普利姆算法重点是选点;克鲁斯卡尔算法重点是选边。
普利姆(Prim)算法
假设G=(V,E)
是一个具有n
个顶点的带权连通图,T=(U,TE)
是G
的最小生成树,其中U
是T
的顶点集,TE
是T
的边集,则由G
构造从起始点v
出发的最小生成树T
的步骤如下:
- 初始化
U={v}
,以v
到其他顶点的所有边为候选边。 - 重复一下两步骤(n-1)次,使得其他(n-1)个顶点被加入到
U
中。- 从候选边中挑选权值最小的的边加入
TE
,设该边在V-U
中的顶点是k
,讲k
加入U
中; - 考察当前
V-U
中的所有顶点j
,修改候选边,若(k, j)
的权值小于原来和顶点j
关联的候选边,则用(k ,j)
取代后者作为侯选边。
- 从候选边中挑选权值最小的的边加入
Prim是一种增量算法,一步步地选择最小边,并在U
集合中添加相应的顶点。
注意,不一定是链式的。要计算非生成树的结点倒生成树结点中的最小距离(有边就都要遍历维护最小值)。书中的例子是特例。
普通邻接表实现
这里是利用类似于dfs套bfs的感觉。通过遍历每一个节点的链表,链表上的点就可能是将要走的点。寻找没有走过,且边权最小的点为下一个个节点。
核心代码
1 | bool st[MaxV]; // 判断点是否被遍历过 |
完整代码
1 |
|
邻接矩阵实现
存储结构
1 | int n; // n表示图中的结点数 |
模板
1 | void Prim() { |
核心代码
1 | // 如果图不连通,则返回INF, 否则返回最小生成树的树边权重之和 |
完整代码
1 |
|
邻接矩阵实现——教材
核心代码
1 | void Prim(MatGraph g, int v) { |
完整代码
1 |
|
克鲁斯卡尔(Kruskal)算法
克鲁斯卡尔算法是一种按权值的递增次序选择合适的边来构造最小生成树的方法。
假设 G = (V, E)
是一个具有 n
个顶点的带权联通无向图, T = (U, TE)
是 G
的最小生成树,则钩爪最小生成树的步骤如下:
- 置
U
的初始值为V
(即包含有G
中的全部顶点),TE
的初始为空集(即图中的每一个顶点都构成一个分量)。 - 将图
G
中的边按权值从小到大的顺序依次选取,若选取的边未使生成树T
形成回路,则加入TE
,否则舍弃,直到TE
中包含n-1
边为止。
邻接矩阵实现——教材
核心代码
- 定义一个表示边的结构体。这样方便统计边的前后结点。而且注意:Kruskal算法关注边。
- 定义一个排序函数(或者在构造结构体时,顺便实现排序方式,这样就可以使用
sort()
)- 定义
Kruskal
函数。函数首先根据参数g
创建边的集合,即E
。创建好集合后,排序。这样操作之后,E数组就是排好序的边了。现在只需要挨个从小到达遍历这些边是否符合条件就行。- 定义一个辅助函数。这个函数的下标
i
记录结点i
的种类。初始时赋值为i
即所有结点的种类都是单独的。当两个或多个结点的种类为同一个时,则这些点被连接起来了,也就是在同一颗最小生成树上了。- 判断符合条件——是否构成回路。如果两个端点在一个集合中,即已经渲染为同类了,就是一个回路,如果不是同类,就使用该边。并且需要将两个端点所在的子生成树的所有结点都改成同一个,表示渲染成同一类。
1 | // Kruskal算法需要的结构体 |
完整代码
1 |
|
改进克鲁斯卡尔算法
改进克鲁斯卡尔算法需要 并查集 和 堆排序 的基础,如果不会可以点击链接学习。
完整代码如下:
1 |
|
如果使用更快的排序,例如堆排序,快速排序使得排序的时间复杂度提升为 $O(mlog_2 m)$,当然选择排序方式应当选择更快速,且更稳定的算法,所有这里选择使用堆排序。并且Kruskal算法是加边操作,很容易造成:多个边提前并在一个集合中,并且产生多个这个集合,在原本的Kruskal算法中合并集合占用的时间复杂度很高,但是思考这一性质,可以想出利用并查集来优化,使用并查集合并集合的时间复杂度为,所以while循环整体的时间复杂度变为。对于连通无向图来说,,所以最终算法时间复杂度为。
适用于竞赛的简化版Kruskal
1 | int n, m; // n是点数,m是边数 |