最小生成树
最小生成树(Minimum Spanning Tree,MST)是在一个给定的无向图G(V,E)中求一棵树 T,使得这棵树拥有图 G中的所有顶点,且所有边都是来自图 G 中的边,并且满足整棵树的边权之和最小。
最小生成树有三个性质需要掌握:
- 最小生成树是树,因此其边数等于顶点数减 1,且树内一定不会有环
- 对给定的图G(V,E),其最小生成树可以不唯一,其边权之和一定是唯一的
- 由于最小生成树是无向图上生成的,因此其根结点可以使这棵树上的任意一个结点。
一般来说,如果题目中涉及最小生成树本身的输出,为了让最小生成树唯一,一般都会直接给出根结点。
求解最小生成树一般有两种算法:Prim 算法和 Kruskal 算法,都采用了贪心的思想,只是贪心的策略不一样。
Prim算法
基本思想
对于图 G(V, E) 设置集合 S,存放已经被访问的顶点,然后每次从集合 V-S 中选择集合 S 的最短距离最小的一个顶点(记为 u),访问并加入集合 S。之后,令顶点 u 为中介点,优化所有从 u 能到达的顶点 v 与集合 S 之间的最短距离。这样的操作执行 n 次(n为顶点个数),直到集合 S 已经包含所有顶点。
也就是执行 n 次下面两个步骤:
- 每次从集合 V - S 中选择与集合 S 最近的一个顶点(记为 u),访问 u 并将其加入集合 S,同时把这条离集合 S 最近的边加入最小生成树中。
- 令顶点 u 作为集合 S 与集合 V-S 连接的接口,优化从 u 能到达的未访问顶点 v 与集合 S 的最短距离。
可以看到 Prim 算法的思想与最短路径中 Dijkstra 算法的思想几乎相同,只是在设计最短距离的时候用集合 S 代替了 Dijkstra 中的起点 s。
具体实现
prim 算法需要实现两个关键的概念,即集合 S 的实现、顶点Vi 与集合 S 的最短距离。
- 集合 S 的实现方法和 Dijkstra 中相同,即使用一个 bool型数组 vis[] 来表示顶点是否已被访问。
- 不妨令 int 型数组 d[] 来存放顶点 Vi 与集合 S 的最短距离。初始时除了起点 s 的d[s] 赋为0,其余顶点都赋值为一个很大的数 INF,即不可达。
可以看到 prim 算法与 **Dijkstra **算法使用的思想几乎完全相同,只有在数组 d[] 的含义上有所区别。在 Dijkstra 中 d[] 含义为起点 s 到达 Vi 的最短距离,在 prim 算法中 d[] 含义为顶点 Vi 到集合 S 的最短距离,二者的区别仅仅在于最短距离是顶点 Vi 针对的是起点s还是集合V。
所以伪代码和 Dijkstra 很像:
// 数组 d 为顶点与集合 S 的最短距离
void Prim(G, d[]) {
初始化;
for(循环 n 次) {
u = 使 d[u] 最小的还未访问的顶点的标号;
记 u 已经被访问;
for(从 u 出发能到达的所有顶点 v) {
if(v 未被访问 && 以 u 为中介点使得 v 与集合 S 的最短距离 d[v] 更优) {
将 G[u][v] 赋值给 v 与集合 S 的最短距离 d[v];
}
}
}
}
具体实现:
邻接矩阵版
const int maxn = 1000; // 最大顶点数
const INF = 1e9;
int n, G[maxn][maxn]; // n 为顶点数
int d[maxn]; // 顶点与 S 的最短距离
bool vis[maxn] = {
false}; // 标记数组
int prim(int s)
{
// s 号为默认初始点,函数返回最小生成树的边权之和
fill(d, d+maxn, INF); // fill 函数整个数组赋值为 INF
d[s] = 0; // 只有顶点到集合 S 的距离为 0&#