算法:图解最小生成树之普里姆(Prim)算法

我们在图的定义中说过,带有权值的图就是网结构。一个连通图的生成树是一个极小的连通子图,它含有图中全部的顶点,但只有足以构成一棵树的n-1条边。所谓的最小成本,就是n个顶点,用n-1条边把一个连通图连接起来,并且使得权值的和最小。综合以上两个概念,我们可以得出:构造连通网的最小代价生成树,即最小生成树(Minimum Cost Spanning Tree)

找连通图的最小生成树,经典的有两种算法,普里姆算法和克鲁斯卡尔算法,这里介绍普里姆算法。

为了能够讲明白这个算法,我们先构造网图的邻接矩阵,如图7-6-3的右图所示。



也就是说,现在我们已经有了一个存储结构为MGraph的MG(见邻接矩阵创建图》)。MG有9个顶点,它的二维数组如右图所示,数组中我们使用65535代表无穷。

下面我们对着程序和每一步循环的图示来看:

算法代码:(改编自《大话数据结构》)

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/* Prim算法生成最小生成树  */
void MiniSpanTree_Prim(MGraph MG)
{
     int min, i, j, k;
     int adjvex[MAXVEX]; /* 保存相关顶点下标 */
     int lowcost[MAXVEX]; /* 保存相关顶点间边的权值 */
    lowcost[ 0] =  0; /* 初始化第一个权值为0,即v0加入生成树 */
     /* lowcost的值为0,在这里就是此下标的顶点已经加入生成树 */
    adjvex[ 0] =  0; /* 初始化第一个顶点下标为0 */
    cout <<  "最小生成树的边为:" << endl;
     for (i =  1; i < MG.numVertexes; i++)
    {
        lowcost[i] = MG.arc[ 0][i]; /* 将v0顶点与之有边的权值存入数组 */
        adjvex[i] =  0; /* 初始化都为v0的下标 */
    }

     for (i =  1; i < MG.numVertexes; i++)
    {
        min = INFINITY;  /* 初始化最小权值为∞, */

        j =  1;
        k =  0;

         while (j < MG.numVertexes) /* 循环全部顶点 */
        {
             if (lowcost[j] !=  0 && lowcost[j] < min) /* 如果权值不为0且权值小于min */
            {
                min = lowcost[j]; /* 则让当前权值成为最小值 */
                k = j; /* 将当前最小值的下标存入k */
            }

            j++;
        }

        cout <<  "(" << adjvex[k] <<  ", " << k <<  ")" <<  "  "/* 打印当前顶点边中权值最小的边 */
        lowcost[k] =  0; /* 将当前顶点的权值设置为0,表示此顶点已经完成任务 */

         for (j =  1; j < MG.numVertexes; j++) /* 循环所有顶点 */
        {
             /* 如果下标为k顶点各边权值小于此前这些顶点未被加入生成树权值 */
             if (lowcost[j] !=  0 && MG.arc[k][j] < lowcost[j])
            {
                lowcost[j] = MG.arc[k][j]; /* 将较小的权值存入lowcost相应位置 */
                adjvex[j] = k; /* 将下标为k的顶点存入adjvex */
            }
        }
    }
    cout << endl;
}


1、程序中1~16行是初始化操作,其中第7~8行 adjvex[0] = 0 意思是现在从顶点v0开始(事实上从那一点开始都无所谓,假定从v0开始),lowcost[0]= 0 表示v0已经被纳入到最小生成树中,之后凡是lowcost数组中的值被设为0就表示此下标的顶点被纳入最小生成树。

2、第11~15行表示读取邻接矩阵的第一行数据,所以 lowcost数组为{ 0 ,10, 65535, 65535, 65535, 11, 65535, 65535, 65535 },而adjvex数组为全0。至此初始化完毕。

3、第17~49行共循环了8次,i从1一直累加到8,整个循环过程就是构造最小生成树的过程。

4、第24~33行,经过循环后min = 10, k = 1。注意26行的if 判断lowcost[j] != 0 表示已经是生成树的顶点则不参加最小权值的查找。

5、第35行,因k = 1, adjvex[1] = 0, 所以打印结果为(0, 1),表示v0 至 v1边为最小生成树的第一条边,如下图的第一个小图。

6、第36行,因k = 1 将lowcost[k] = 0 就是说顶点v1纳入到最小生成树中,此时lowcost数组为{ 0,0, 65535, 65535, 65535, 11, 65535, 65535, 65535 }

7、第38~47行,j 循环从1 到8, 因k = 1,查找邻接矩阵的第v1行的各个权值,与lowcost数组对应值比较,若更小则修改lowcost值,并将k值存入adjvex数组中。所以最终lowcost = { 0,0, 18, 65535, 65535, 11, 16, 65535, 12 }。 adjvex数组的值为 {0, 0, 1, 0, 0, 0, 1, 0, 1 }。这里的if判断也表示v0和v1已经是生成树的顶点不参与最小权值的比对了。


上面所述为第一次循环,对应下图i = 1的第一个小图,由于要用文字描述清楚整个流程比较繁琐,下面给出i为不同值一次循环下来后的生成树图示,所谓一图值千言,大家对着图示自己模拟地循环8次就能理解普里姆算法的思想了。


即最小生成树的边为:(0, 1), (0, 5), (1, 8), (8, 2), (1, 6), (6, 7), (7, 4), (7, 3)

最后再来总结一下普里姆算法的定义:

假设N = (V{E} )是连通网,TE是N上最小生成树的集合。算法从U = { u0} ( uo V),TE = { } 开始。重复执行下述操作:在所有

uU,v V - U 的边(u, v) E 中找一条代价最小的边(u0 , v0) 并入集合TE, 同时v0 并入U, 直至 U = V 为止。此时TE 中必有n-1 条边, 则 T = (V,{TE} ) 为N的最小生成树。


由算法代码中的循环嵌套可得知此算法的时间复杂度为O(n^2)。

对比普里姆和克鲁斯卡尔算法,克鲁斯卡尔算法主要针对边来展开,边数少时效率比较高,所以对于稀疏图有较大的优势;而普里姆算法对于稠密图,即边数非常多的情况下更好一些。

  • 21
    点赞
  • 89
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
最小生成树是指在一个连通图中,找到一棵包含所有顶点且权值最小的生成树。普里姆算法和克鲁斯卡尔算法都是常用的求解最小生成树算法。 1. 普里姆算法: - 算法思路: - 从图中任意选择一个顶点作为起始点,将其加入最小生成树中。 - 从已加入最小生成树的顶点集合中,选择一个顶点v,将与v相连的边中权值最小的边(u, v)加入最小生成树中,并将顶点u加入最小生成树的顶点集合中。 - 重复上一步,直到最小生成树包含了图中所有的顶点。 - 算法实现(邻接矩阵存储): ```python def prim(graph): num_vertices = len(graph) selected = [False] * num_vertices selected[0] = True for _ in range(num_vertices - 1): min_weight = float('inf') u, v = -1, -1 for i in range(num_vertices): if selected[i]: for j in range(num_vertices): if not selected[j] and graph[i][j] < min_weight: min_weight = graph[i][j] u, v = i, j selected[v] = True print(f"Add edge ({u}, {v}) with weight {min_weight} to the minimum spanning tree.") # 示例图的邻接矩阵表示 graph = [ [0, 2, 0, 6, 0], [2, 0, 3, 8, 5], [0, 3, 0, 0, 7], [6, 8, 0, 0, 9], [0, 5, 7, 9, 0] ] prim(graph) ``` 2. 克鲁斯卡尔算法: - 算法思路: - 将图中的所有边按照权值从小到大进行排序。 - 依次选择权值最小的边,如果这条边的两个顶点不在同一个连通分量中,则将这条边加入最小生成树中,并将这两个顶点合并到同一个连通分量中。 - 重复上一步,直到最小生成树包含了图中所有的顶点。 - 算法实现(邻接表存储): ```python class Edge: def __init__(self, src, dest, weight): self.src = src self.dest = dest self.weight = weight class Graph: def __init__(self, num_vertices): self.num_vertices = num_vertices self.edges = [] def add_edge(self, src, dest, weight): self.edges.append(Edge(src, dest, weight)) def find(parent, i): if parent[i] == i: return i return find(parent, parent[i]) def union(parent, rank, x, y): xroot = find(parent, x) yroot = find(parent, y) if rank[xroot] < rank[yroot]: parent[xroot] = yroot elif rank[xroot] > rank[yroot]: parent[yroot] = xroot else: parent[yroot] = xroot rank[xroot] += 1 def kruskal(graph): result = [] i, e = 0, 0 graph.edges = sorted(graph.edges, key=lambda x: x.weight) parent = [] rank = [] for node in range(graph.num_vertices): parent.append(node) rank.append(0) while e < graph.num_vertices - 1: edge = graph.edges[i] i += 1 x = find(parent, edge.src) y = find(parent, edge.dest) if x != y: e += 1 result.append(edge) union(parent, rank, x, y) for edge in result: print(f"Add edge ({edge.src}, {edge.dest}) with weight {edge.weight} to the minimum spanning tree.") # 示例图的邻接表表示 graph = Graph(5) graph.add_edge(0, 1, 2) graph.add_edge(0, 3, 6) graph.add_edge(1, 2, 3) graph.add_edge(1, 3, 8) graph.add_edge(1, 4, 5) graph.add_edge(2, 4, 7) graph.add_edge(3, 4, 9) kruskal(graph) ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值