最小生成树
生成树:所谓的一个连通图的生成树是一个极小的连通子图,它含有图中全部的n个顶点,但只有足以构成一棵树的n-1条边。
**给定一个带权的无向连通图,如何选取一棵生成树,使树上所有边上权的总和为最小,这叫最小生成树 **
-
N个顶点,一定有N-1条边
-
包含全部顶点
-
N-1条边都在图中
普里姆(Prim)算法
假设N= (P,{E}) 是连通网,TE是N上最小生成树中边的集合。算法从U={uo}(uo∈V)(V中某一顶点 u0 ),TE={}开始,重复执行以下操作。在所有u∈U,v∈V-∪的边(u,v) ∈E中(一个顶点在U中,另一个顶点在V-U中).找一条代价最小的边(uo,Vo) 并入集合TE,同时vo并入U,直至U=V为止。此时TE中必有n-1条边,则T= (V,{TE}) 为N的最小生成树。
普里姆算法在找最小生成树时,将顶点分为两类,一类是在查找的过程中已经被纳入树中的顶点(A类)。另一类是未被纳入的顶点(B类)。(下面代码以lowcost的值分为A、B类。等于0是A类,不等于0是B类)
对于给定的连通网,起始状态全部顶点都归为B 类。在找最小生成树时,选定任意一个顶点作为起始点,并将之从 B 类移至 A 类;然后找出 B 类中到 A 类中的顶点之间权值最小的顶点,将之从 B 类移至 A 类,如此重复,直到 B 类中没有顶点为止。所走过的顶点和边就是该连通图的最小生成树。
由算法代码中的循环嵌套可得知此算法的时间复杂度为0(n2)
实现
1 static void MiniSpanTree_Prim(MGraph m) {
2 // 保存相关顶点的下标,对应lowcost权值的起始点
3 int[] adjvex = new int[9];
4 // 保存相关顶点间边的权值,为0时代表顶点被纳入最小生成树
5 int[] lowcost = new int[9];
6 int min, j, k;
7 // 初始化第一个权值为0,即V0加入生成树。lowcost的值为0,代表此下标的顶点加入到生成树。
8 lowcost[0] = 0;
9 // 初始化第一个顶点下标为0
10 adjvex[0] = 0;
11 // 循环除下标为0外的全部顶点
12 for (int i = 1; i < m.numVertexes; i++) { // 将V0顶点有边的权值存入数组
13 lowcost[i] = m.arc[0][i]; // 初始化下标为0
14 adjvex[i] = 0;
15 }
16 for (int i = 1; i < m.numVertexes; i++) {
17 min = INFINITY;
18 j = 1;
19 k = 0;
20 while(j < m.numVertexes) {
21 // 如果权值不为0且权值小于min
22 if(lowcost[j]!= 0 && lowcost[j] < min) {
23 min = lowcost[j];
24 k = j;
25 }
26 j++;
27 }
28 // 打印当前顶点边中权值最小边
29 System.out.print(adjvex[k] + " " + k + ",");
30 // 把当前顶点的权值设置为0,表示此顶点已完成任务
31 lowcost[k] = 0;
32 for (j = 1; j < m.numVertexes; j++) {
33 // 若下标为k顶点各边权值小于此前这些顶点未被加入生成树权值
34 if(lowcost[j]!= 0 && m.arc[k][j] < lowcost[j]) {
35 // 将较小权值存入lowcost
36 lowcost[j] = m.arc[k][j];
37 // 将下标为k的顶点存入adjvex
38 adjvex[j] = k;
39 }
40 }
41 }
42}
- 8-10:我们分别给这两个数组的第一个下标位赋值为0,arjvex[0]=0 其实意思就是我们现在从顶点Vo 开始(事实上,最小生成树从哪个顶点开始计算都无所谓,我们假定从Vo开始),lowcost[0]=0就表示Vo已经被纳入到最小生成树中,之后凡是lowcost数组中的值被设置为0就是表示此下标的顶点被纳入最小生成树。
- 12-14:for循环,我们读取邻接矩阵的第一行数据。将数值赋值给lowcost 数组,所以此时lowcost 数组值为 {0,10,65535,65535,65535,11,65535, 65535, 65535}, 而arjvex则全部为0。 此时,我们已经完成了整个初始化的工作,准备开始生成。
- 16-41:整个循环过程就是构建最小生成树的过程。注意由于我们已经把V0纳入到最小生成树了,循环从一开始。整个循环主要分为3部分(1).找到当前lowcost数组中最小值,并用k保留(2)打印结果(3)查找对比k行的权值并放入lowcost数组
- 17-27:找到当前lowcost数组中最小值,并用k保留此最小值的顶点下标。经过循环后,min=10, k=1.注意22行if判断的lowcost[j]!=0表示已经是生成树的顶点不参与最小权值的查找。
- 29-31:因k=1, adjvex[1]=0,打印结果为(0, 1),表示v0至v1边为最小生成树的第一条边。把当前顶点(k)的权值设置为0,表示此顶点已被纳入到最小生成树
- 32-40:j循环由1至8,因k=1,查找邻接矩阵的第V1行的各个权值,与lowcost的对应值比较,若更小则修改lowcost 值,并将k值存入adjvex数组中。因第v1行有18、16、12均比65535小,所以最终lowcost数组的值为: {0,0,18,65535,65535,11,16,65535,12}。 adjvex 数组的值为:{0,0,1,0,0,0,1,0,1}。这里if判断的lowcost[j]!=0也说明V0和V1已经是生成树的顶点不参与最小权值的比对了。
- 再次执行16-41行,得到第二条边(0, 5)。
构造最小生成树的过程
克鲁斯卡尔(Kruskal)算法
克鲁斯卡尔算法的基本思想是以边为主导地位,
假设N= (V,{E}) 是连通网,则令最小生成树的初始状态为只有n个顶点而无边的非连通图T={V,{}},图中每个顶点自成一个连通分量。在E中选择代价最小的边,若该边依附的顶点落在T中不同的连通分量上,则将此边加入到T中,否则舍去此边而选择下一条代价最小的边。依次类推,直至T中所有顶点都在同一连通分量上为止。
此算法的Find函数由边数e决定,时间复杂度为0(loge),而外面有一个for循环e次。所以克鲁斯卡尔算法的时间复杂度为O(eloge)。
具体思路
- 将边按权值从小到大的顺序添加到新图中,保证添加的过程中不会形成环
- 重复上一步直到连接所有顶点,此时就生成了最小生成树
判断是否会产生回路的方法
在初始状态下给每个顶点赋予不同的标记,对于遍历过程的每条边,其都有两个顶点,判断这两个顶点的标记是否一致,如果一致,说明它们本身就处在一棵树中,如果继续连接就会产生回路;如果不一致,说明它们之间还没有任何关系,可以连接。(代码中使用int[] parent来标记是否会生成环路)
实现
1static void MiniSpanTree_kruskal(MGraph m) {
2 // 定义数组判断边与边是否形成环路。
3 int[] parent = new int[m.numVertexes];
4 // 返回边集数组,权值从小到大排序
5 List<Edge> edgeList= getEdgeList();
6 // 初始化数组值为0
7 for (int i = 0; i < m.numVertexes; i++) {
8 parent[i] = 0;
9 }
10 for (int i = 0; i < m.numVertexes; i++) {
11 int x =findParent(parent, edgeList.get(i).begin);
12 int y = findParent(parent, edgeList.get(i).end);
13 // 假如x与y不等,说明此边没有与现有生成树形成环路
14 if(x != y) {
15 // 将此边的结尾顶点放入到下标为起点的parent中。表示此边已经在生成树集合中
16 parent[x] = y;
17 System.out.println("(" + edgeList.get(i).begin + "," + edgeList.get(i).end + ")," + edgeList.get(i).weight);
18 }
19 }
20}
// 查找连线顶点的尾部下标
21static int findParent(int[] parent, int f) {
22 while(parent[f] > 0) {
23 f = parent[f];
24 }
25 return f;
26}
分析代码的执行步骤
1-9:是对循环做的准备工作
10-19:对输入的边集数组进行循环遍历
-
i=0:调用函数findParent,传入的参数是数组parent和当前权值最小边(V4,V7) 的begin: 4。因为parent中全都是0所以传出值使得n=4。传入(V4,V7) 的end: 7。传出值使得m=7。n与m不相等,因此parent[4]=7。 此时parent数组值为{0,0, 0, 0, 7, 0, 0, 0, 0},此时我们已经将边(V4,V7)纳入到最小生成树中。
-
i=1、2、3、4、5时,如图
-
-
当i=6时,此时parent数组为{1,5,8,7,7,8,0,0,6}。由i=6的粗线连线可以得到,我们其实是有两个连通的边集合A与B中纳入到最小生成树中。
-
当parent[0]=1, 表示v0和V1已经在生成树的边集合A中。此时将parent[0]=1的1改为下标,由parent[1]=5,表示V1和V5在边集合A中,parent[5]=8 表示Vs与V8在边集合A中,parent[8]=6表示v8与V6在边集合A中,parent[6]=0表示集合A暂时到头,我们查看parent中没有查看的值,parent[2]=8 表示V2与V8在一个集合中,因此v2也在边集合A中。再由parent[3]=7、parent[4]=7和parent[7]=0可知此时边集合A(V0、 V1、V2、V5、V8、V6)。集合B中(V3、V4、V7)。
-
当i=7时,第10行,调用Find函数,会传入参数edges[7].begin=5,得到n=6。传入参数edges[7].end=6得到m=6。此时n=m。这就告诉我们,因为边(V5,V6)使得边集合A形成了环路。因此不能将它纳入到最小生成树中。当i=8时,与上面相同,由于边(V1,V2) 使得边集合A形成了环路。
-
当i=9时,边(v6,V7),第11行得到n=6,第12行得到m=7,因此parent[6]=7。 此时parent 数组值为{1, 5, 8, 7, 7,8,7, 0,6}。此后边的循环均造成环路。
最终最小生成树
普里姆算法和克鲁斯卡尔算法的区别
克魯斯卡尔算法主要是针对边来展开,边数少时效率会非常高,所以对于稀疏图有很大的优势;普里姆算法对于稠密图,即边数非常多的情况会更好一些。