连通图中寻找最小生成树的常用算法有 2 种,分别是普里姆算法和克鲁斯卡尔算法。本节,我们将带您详细了解克鲁斯卡尔算法。
和普里姆算法类似,克鲁斯卡尔算法的实现过程也采用了贪心的策略:对于具有 n 个顶点的图,将图中的所有路径(边)按照权值大小进行升序排序,从权值最小的路径开始挑选,只要此路径不会和已选择的其它路径构成环路,就选定其作为最小生成树的一部分,直至选够 n-1 条路径。
对于具有 n 个顶点的图,选择 n-1 条路径就可以将所有顶点连接起来。在此基础上,保证所选的每条路径的权值都最小,就可以找到一棵最小生成树。
图 1 图存储结构
以图 1 所示的连通图为例,克鲁斯卡尔算法寻找最小生成树的过程为:
1) 将所有路径(边)按照权值大小进行升序排序:
2) 从最小的路径开始,只要该路径不会和其它已选路径产生环路,就选择它作为组成最小生成树的一部分。显然 (b,d) 符合要求,选择它组成最小生成树:
图 2 克鲁斯卡尔算法寻找最小生成树_过程 1
3) (d,t) 不会和已选路径 (b,d) 构成环路,可以组成最小生成树:
图 3 克鲁斯卡尔算法寻找最小生成树_过程 2
4) (a,c) 不会和 (b,d)、(d,t) 构成环路,可以组成最小生成树:
图 4 克鲁斯卡尔算法寻找最小生成树_过程 3
5) (c,d) 不会和 (a,c)、(b,d)、(d,t) 构成环路,可以组成最小生成树:
图 5 克鲁斯卡尔算法寻找最小生成树_过程 4
6) (c,b) 会和已选路径 (c,d)、(b,d) 构成环路(如图 6 所示),因此不会被选择:
图 6 克鲁斯卡尔算法寻找最小生成树_过程 5
7) (b,t) 会和已选路径 (b,d)、(d,t) 构成环路,也不被选择;
8) (a,b) 会和已选路径 (a,c)、(c,d)、(d,b) 构成环路,也不被选择;
9) (s,a) 不会和已选路径 (a,c)、(c,d)、(d,b)、(d,t) 构成环路,可以组成最小生成树:
图 7 克鲁斯卡尔算法寻找最小生成树_过程 6
图 1 中的图结构共有 6 个顶点,我们已经选择了 5 条路径,因此算法执行结束,图 7 所示即为最终找到的最小生成树。
克鲁斯卡尔算法的具体实现
克鲁斯卡尔算法的实现,难点在于如何判断所选路径是否会造成环路,这里给您介绍一种简单的百家乐凯发k8的解决方案:初始状态下,为图中的每个顶点配备一个互不相同的标记值,算法执行过程中,如果新路径两个顶点的标记值不同,则不会构成环路,该路径被选择的同时,要将两个顶点的标记改为和其它已选路径中的顶点标记相同;反之,如果该路径两个顶点的标记值相同,则会构成环路。
举个例子,图 5 中,已选路径为 (a,c)、(b,d)、(c,d)、(d,t),此时顶点 a、c、b、d、t 的标记值相同,顶点 s 的标记值和它们不同。图 6 中,判定 (b,c) 路径是否可以组成最小生成树时,由于顶点 b 和 c 的标记值相同,因此该路径会和其它已选路径构成环路(如图 6 所示),不能组成最小生成树。
如下为实现克鲁斯卡尔算法的 c 语言程序:
#include#include #define n 9 // 图中边的数量 #define p 6 // 图中顶点的数量 //构建表示边的结构体 struct edge { //一条边有 2 个顶点 int initial; int end; //边的权值 int weight; }; //qsort排序函数中使用,使edges结构体中的边按照权值大小升序排序 int cmp(const void *a, const void*b) { return ((struct edge*)a)->weight - ((struct edge*)b)->weight; } //克鲁斯卡尔算法寻找最小生成树,edges 存储用户输入的图的各个边,mintree 用于记录组成最小生成树的各个边 void kruskal_mintree(struct edge edges[], struct edge mintree[]) { int i,initial, end; //每个顶点配置一个标记值 int assists[p]; int num = 0; //初始状态下,每个顶点的标记都不相同 for (i = 0; i < p; i ) { assists[i] = i; } //根据权值,对所有边进行升序排序 qsort(edges, n, sizeof(edges[0]), cmp); //遍历所有的边 for (int i = 0; i < n; i ) { //找到当前边的两个顶点在 assists 数组中的位置下标 initial = edges[i].initial - 1; end = edges[i].end - 1; //如果顶点位置存在且顶点的标记不同,说明不在一个集合中,不会产生回路 if (assists[initial] != assists[end]) { //记录该边,作为最小生成树的组成部分 mintree[num] = edges[i]; //计数 1 num ; int elem = assists[end]; //将新加入生成树的顶点标记全部改为一样的 for (int k = 0; k < p; k ) { if (assists[k] == elem) { assists[k] = assists[initial]; } } //如果选择的边的数量和顶点数相差1,证明最小生成树已经形成,退出循环 if (num == p - 1) { break; } } } } void display(struct edge mintree[]) { int cost = 0; printf("最小生成树为:\n"); for (int i = 0; i < p - 1; i ) { printf("%d-%d 权值:%d\n", mintree[i].initial, mintree[i].end, mintree[i].weight); cost = mintree[i].weight; } printf("总权值为:%d", cost); } int main() { int i; struct edge edges[n], mintree[p - 1]; for (i = 0; i < n; i ) { scanf("%d %d %d", &edges[i].initial, &edges[i].end, &edges[i].weight); } kruskal_mintree(edges,mintree); display(mintree); return 0; }
如下为实现克鲁斯卡尔算法的 java 程序:
import java.util.arrays; import java.util.scanner; public class prim { static int n = 9; // 图中边的数量 static int p = 6; // 图中顶点的数量 //构建表示路径的类 public static class edge implements comparable{ //每个路径都有 2 个顶点和 1 个权值 int initial; int end; int weight; public edge(int initial, int end, int weight) { this.initial = initial; this.end = end; this.weight = weight; } //对每个 edge 对象根据权值做升序排序 @override public int compareto(edge o) { return this.weight - o.weight; } } public static void kruskal_mintree(edge[] edges,edge [] mintree) { int []assists = new int[p]; //每个顶点配置一个不同的标记值 for (int i = 0; i < p; i ) { assists[i] = i; } //根据权值,对所有边进行升序排序 arrays.sort(edges); //遍历所有的边 int num = 0; for (int i = 0; i < n; i ) { //找到当前边的两个顶点在 assists 数组中的位置下标 int initial = edges[i].initial - 1; int end = edges[i].end - 1; //如果顶点位置存在且顶点的标记不同,说明不在一个集合中,不会产生回路 if (assists[initial] != assists[end]) { //记录该边,作为最小生成树的组成部分 mintree[num] = edges[i]; //计数 1 num ; int elem = assists[end]; //将新加入生成树的顶点标记全不更改为一样的 for (int k = 0; k < p; k ) { if (assists[k] == elem) { assists[k] = assists[initial]; } } //如果选择的边的数量和顶点数相差1,证明最小生成树已经形成,退出循环 if (num == p - 1) { break; } } } } public static void display(edge [] mintree) { system.out.println("最小生成树为:"); int cost = 0; for (int i = 0; i < p - 1; i ) { system.out.println(mintree[i].initial " - " mintree[i].end " 权值为:" mintree[i].weight); cost = mintree[i].weight; } system.out.print("总权值为:" cost); } public static void main(string[] args) { scanner scn = new scanner(system.in); edge[] edges = new edge[n]; edge[] mintree = new edge[p-1]; system.out.println("请输入图中各个边的信息:"); for(int i=0;i
如下为实现克鲁斯卡尔算法的 python 程序:n = 9 #图中边的数量 p = 6 #图中顶点的数量 #构建表示边的结构体 class edge: #一条边有 2 个顶点 initial = 0 end = 0 #边的权值 weight = 0 def __init__(self,initial,end,weight): self.initial = initial self.end = end self.weight = weight edges = [] # 用于保存用户输入的图各条边的信息 mintree=[] # 保存最小生成数各个边的信息 #输入 n 条边的信息 for i in range(n): li = input().split() initial = int(li[0]) end = int(li[1]) weight = int(li[2]) edges.append(edge(initial,end,weight)) # 根据 weight 给 edges 列表排序 def cmp(elem): return elem.weight #克鲁斯卡尔算法寻找最小生成树 def kruskal_mintree(): #记录选择边的数量 num = 0 #为每个顶点配置一个不同的标记 assists = [i for i in range(p)] #对 edges 列表进行排序 edges.sort(key = cmp) #遍历 n 条边,从重选择可组成最小生成树的边 for i in range(n): #找到当前边的两个顶点在 assists 数组中的位置下标 initial = edges[i].initial -1 end = edges[i].end-1 # 如果顶点位置存在且顶点的标记不同,说明不在一个集合中,不会产生回路 if assists[initial] != assists[end]: # 记录该边,作为最小生成树的组成部分 mintree.append(edges[i]) #计数 1 num = num 1 #将新加入生成树的顶点标记全部改为一样的 elem = assists[end] for k in range(p): if assists[k] == elem: assists[k]= assists[initial] #如果选择的边的数量和顶点数相差1,证明最小生成树已经形成,退出循环 if num == p-1: break def display(): cost = 0 print("最小生成树为:") for i in range(p-1): print("%d-%d 权值:%d"%(mintree[i].initial, mintree[i].end, mintree[i].weight)) cost = cost mintree[i].weight print("总权值为:%d"%(cost)) kruskal_mintree() display()
以图 1 所示的图结构为例,以上程序的输出结果均为:5 1 7
5 3 8
1 2 6
1 3 3
3 2 4
3 4 3
2 4 2
2 6 5
4 6 2
最小生成树为:
2-4 权值:2
4-6 权值:2
1-3 权值:3
3-4 权值:3
5-1 权值:7
总权值为:17