最小生成树算法

Posted fsmly

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了最小生成树算法相关的知识,希望对你有一定的参考价值。

最小生成树的形成
  (1)一个贪心策略设计如下
       每个时刻生长最小生成树的一条边,并在整个策略的实施过程中,遵守下述循环不变式的边集合A:
          技术分享图片
  每一步,选择一条边(u,v)加入集合A,使得A不违反循环不变式。
  这样的边使得我们可以“安全地”将之加入到集合A而不会破坏A的循环不变式,因此称之为集合A的“安全边”。
  技术分享图片

  (2)使用的循环不变式方式如下:
  初始化:集合A直接满足循环不变式。
  保持:算法的循环通过只加入安全边来维持循环不变式。
  终止:所有加入到集合A中的边都属于某棵最小生成树,因此,算法第5行所返回的集合A必然是一棵最小生成树。
  说明:循环不变式告诉了我们存在一棵生成树,满足技术分享图片。在进入while循环时,A是T的真子集,因此必然存在一条边技术分享图片,使得技术分享图片,并且(u,v)对于集合A是安全的。
  (3)定义一些概念

  技术分享图片
  切割:无向图G=(V,E)的一个切割(S,V-S)是集合V的一个划分。

  横跨切割:如果一条边(u,v)∈E的一个端点在集合S中,另一个端点在集合V-S中,则称该条边横跨切割(S,V-S)。

  尊重集合:如果集合A中不存在横跨该切割的边,则称该切割尊重集合A。

  轻量级边:在横跨一个切割的所有边中,权重最小的边称为轻量级边

  (4)距离说明上述概念

  技术分享图片

  图(a)中所示的黑色结点位于集合S中,白色结点位于V-S中。横跨该切割的边是那些连接白色结点和黑色结点的边。如<a,h><b,c><c,d>等等,其中<d,c>是轻量级边。若定义加了阴影的边属于集合A,那么可以看出集合A中没有横跨切割的边,所以切割<S,V-S>尊重集合A。

  图(b)中是同样一个图,只是换了视角。

  (5)辨认安全边的规则

  设G=(V,E)是一个在边E上定义了实数值权重函数ω的连通无向图。设集合A为E的一个子集,且A包含在图G的某棵最小生成树中,设(S,V-S)是图G中尊重集合A的任意一个切割,

又设(u,v)是横跨切割(S,V-S)的一条轻量级边。那么边(u,v)对于集合A是安全的。

  证明:

  技术分享图片

  我们现在构建一个最小生成树T ′,通过将A∪{(u,v)}包括在树T ‘中,从而证明(u,v)对集合A来说是安全的。T中包含有G的所有结点,所以(u,v)与T中从结点u到结点v的简单路径p形成一个环。如上图所示。由于结点u,v分别处于切割S,V-S中,T中至少含有一条简单路径p并且横跨该切割。假设(x,y)是这样一条边。由于切割(S,V-S)尊重集合A,所以(x,y)不在集合A中。又因为(x,y)位于T中从u到v的唯一一条简单路径上,将这条边删除会导致T被分为两个连通分量,这时候,将(u,v)加入就能将这两个连通分量连接起来成为一颗心得生成树技术分享图片

  这时候我们来证明T ′是一颗最小生成树。

  由于(u,v)是横跨切割的一条轻量级边,并且边(x,y)也横跨该切割,所以我们有ω(u,v)≤ ω(x,y),所以可以简单得出这样一个关系

    技术分享图片

  又因为T是一颗最小生成树,ω(T) ≤ ω(T ‘),因此T ′也是一颗最小生成树。

  因为 A∈T,且 (x,y)?A,所以A∈T ‘ 。因此技术分享图片。由于T ‘ 是最小生成树,(u,v)对于集合A而言是安全的。

  在算法GENERIC-MST推进的过程中,集合A始终保持无环状态( 每条边都是安全的 )。算法执行的任意时刻,图G A =(V,A)是一个森林。

  while循环执行|V|-1次,每次找出构造最小生成树所需的一条边。每遍循环将树的数量减少1。当整个森林只含有一棵树时,算法终止。

Kruskal算法
  Kruskal和Prim算法是求解最小生成树的两个经典算法。它们都是GENERIC-MST算法的具体细化:

  (1)Kruskal算法找安全边的方法:在所有连接森林中两棵不同树的边中,找权重最小的边(u,v)。设C 1 和C 2 为边(u,v)所连接的两棵树。边(u,v)是C 1 的一条安全边。

  技术分享图片

  (2)简单讲述一下Kruskal算法的工作过程。算法的1~3行将集合A初始化为一个空集,并创建|V|棵树,每棵树仅包含一个结点,作为初始情况。5~8行中的for循环按照权重从低到高的次序对每一条边逐一进行检查。对于每条边(u,v)而言,循环将检查该结点u和结点v是否属于同一棵树。如果是,这条边不能加入,避免形成环路。如果不是,则两个端点分别属于不同的树,算法第7行将吧这些边加入到集合A中,第8行将两棵树进行合并。

  (3)下面给出一个具体的例子看看算法的流程

  ①首先找到最小权重的边为<h,g> = 1

  技术分享图片

   ②继续寻找下一条权重次小的边,以此类推

  技术分享图片

  技术分享图片

  技术分享图片

  技术分享图片

 

  技术分享图片

  ③注意到,(f)图中此时权重为6的边的两个端点i和g都属于同一棵树(<i,c,f,g,h>)中,所以不能加这条边加入,否则会形成环路

  技术分享图片技术分享图片

  ④同③一样,(h)图中的权重为7的边不同加入集合中,避免形成环路(<i,c,f,g,h>)

  技术分享图片技术分享图片

  ⑤同④一样,(j)图中的权重为8的边不同加入集合中,避免形成环路(<a,b,d,c,f,g,h>)

  技术分享图片技术分享图片

  ⑥同理,<e,f>边如果加入集合中,会形成环路<c,d,e,f>;<b,h>不能加入集合中,会形成<a,b,h>环路;<d,f>不能加入集合中,会形成<c,f,d>环路

  技术分享图片

  技术分享图片

  技术分享图片

技术分享图片
  1 package cn.Graph;
  2 
  3 import java.util.ArrayList;
  4 import java.util.Scanner;
  5 
  6 /**
  7  * 使用kruskal算法生成图的MST
  8  * 其中:
  9  * 1.图的顶点存在了数组中
 10  * 2.图的带权边使用了EdgeNode对象,存在了数组中
 11  * 3.边按照权重进行排序利用最小堆进行排序,每次取出最小堆的根节点,便是权最小的边
 12  * 4.每次向MST中添加边肯定添加最小权的边,唯一条件便是不构成环
 13  * 5.上述不构成环是利用等价类<并查集>实现的
 14  * 6.并查集实现:树!每个顶点两个域:parent域&root域!find-union!重量规则!
 15  * @author cy
 16  *
 17  */
 18 public class Kruskal {
 19     boolean[] root;
 20     int[] parent;
 21     int currentSize=0;
 22     int maxSize=0;
 23     EdgeNode[] minHeap=new EdgeNode[20];
 24 
 25     /**
 26      * 初始化每个顶点为一个类
 27      * @param verNum 顶点的数量
 28      */
 29     public void initialize(int verNum){
 30         root=new boolean[verNum+1];
 31         parent=new int[verNum+1];
 32 
 33         for(int vertex=1;vertex<=verNum;vertex++){
 34             parent[vertex]=1;
 35             root[vertex]=true;
 36         }
 37     }
 38 
 39     /**
 40      * 寻找某个顶点元素所在的类
 41      * @param vertex 顶点
 42      * @return 返回的是顶点所在的类
 43      */
 44     public int find(int vertex){
 45         while(!root[vertex]){
 46             vertex=parent[vertex];
 47         }
 48         return vertex;
 49     }
 50 
 51     /**
 52      * 利用重量规则将两个根节点为i,j的类合并
 53      * @param i 根节点为i
 54      * @param j 根节点为j
 55      */
 56     public void union(int i,int j){
 57         if(parent[i]<parent[j]){
 58             parent[j]+=parent[i];
 59             root[i]=false;
 60             parent[i]=j;
 61         }else{
 62             parent[i]+=parent[j];
 63             root[j]=false;
 64             parent[j]=i;
 65         }
 66     }
 67 
 68     /**
 69      * 通过weight构建以EdgeNode为节点的最小堆
 70      * @param edgeNode为带权的边集
 71      */
 72     public void createMinHeap(EdgeNode[] edgeNode){
 73         currentSize=edgeNode.length;
 74         maxSize=minHeap.length;
 75         if(currentSize>=maxSize){
 76             maxSize*=2;
 77             minHeap=new EdgeNode[maxSize];
 78         }
 79         for(int i=0;i<currentSize;i++)
 80             minHeap[i+1]=edgeNode[i];
 81 
 82         int y,c;
 83         for(int i=currentSize/2;i>=1;i--){
 84             EdgeNode node=minHeap[i];
 85             y=node.weight;
 86             c=2*i;
 87             while(c<currentSize){
 88                 if(c<=currentSize && minHeap[c].weight>minHeap[c+1].weight)
 89                     c++;
 90                 if(minHeap[c].weight>=y)
 91                     break;
 92                 minHeap[c/2]=minHeap[c];
 93                 c=c*2;
 94             }
 95             minHeap[c/2]=node;
 96         }
 97     }
 98 
 99     /**
100      * 最小堆删除两种思路,一种和前面一样,就是一直跟踪放在根节点的那个最后一个节点最终插入的位置
101      * 另一种思路便是每一次完成完整的交换然后下一一层在进行同样处理
102      */
103     public EdgeNode deleteMinHeap(){
104         if(currentSize<1)
105             System.out.println("堆已经为空!无法执行删除");
106         EdgeNode node=minHeap[1];
107         minHeap[1]=minHeap[currentSize];
108         currentSize-=1;
109 
110         int c=2,j=1;
111         EdgeNode node1=minHeap[currentSize+1];
112         while(c<=currentSize){
113             if(c<currentSize && minHeap[c].weight>minHeap[c+1].weight)
114                 c++;
115             if(node1.weight<=minHeap[c].weight)
116                 break;
117             minHeap[j]=minHeap[c];
118             j=c;
119             c=c*2;
120         }
121         minHeap[j]=node1;
122         return node;
123     }
124 
125     /**
126      * 根据图的顶点集合带权边集生成MST
127      * @param verArray 顶点集
128      * @param edgeNode 带权边集
129      */
130     public void minSpanningTree(int[] verArray,EdgeNode[] edgeNode){
131         ArrayList<EdgeNode> nodeList=new ArrayList<EdgeNode>();
132 
133         initialize(verArray.length);
134         createMinHeap(edgeNode);
135 
136         for(int i=1;i<=currentSize;i++){
137             System.out.println(minHeap[i].u+" "+minHeap[i].v+" "+minHeap[i].weight);
138         }
139 
140         for(int i=0;i<edgeNode.length;i++){
141             EdgeNode node=deleteMinHeap();
142             int jRoot=find(node.u);
143             int kRoot=find(node.v);
144             if(jRoot!=kRoot){
145                 nodeList.add(node);
146                 union(jRoot,kRoot);
147             }
148         }
149         System.out.println("使用Kruskal算法得到图的最小生成树为:");
150         for(int i=0;i<nodeList.size();i++){
151             System.out.println(nodeList.get(i).u+" "+nodeList.get(i).v+" "+nodeList.get(i).weight);
152         }
153     }
154 
155     public static void main(String[] args) {
156         System.out.println("请输出图的顶点数和边数:");
157         @SuppressWarnings("resource")
158         Scanner scan=new Scanner(System.in);
159         int verNum=scan.nextInt();
160         int edgeNum=scan.nextInt();
161 
162         int[] verArray=new int[verNum];
163         System.out.println("请依次输入顶点:");
164         for(int i=0;i<verNum;i++){
165             int vertex=scan.nextInt();
166             verArray[i]=vertex;
167         }
168 
169         EdgeNode[] edgeNode=new EdgeNode[edgeNum];
170         System.out.println("请依次输入边的顶点和权重:");
171         for(int i=0;i<edgeNum;i++){
172             int u=scan.nextInt();
173             int v=scan.nextInt();
174             int weight=scan.nextInt();
175             EdgeNode node=new EdgeNode();
176             node.u=u;
177             node.v=v;
178             node.weight=weight;
179             edgeNode[i]=node;
180         }
181         Kruskal kruskal=new Kruskal();
182         kruskal.minSpanningTree(verArray,edgeNode);
183     }
184 }
185 class EdgeNode {
186     int weight;
187     int u,v;
188 }
Kruskal算法

 

Prim算法

  (1)Prim算法的每一步是在连接集合A和A之外的结点的所有边中,选择一条轻量级边加入到A中。所加入的边对于A也是安全的。Prim算法的基本性质:集合A中的边总是构成一棵树。

  技术分享图片

   (2)简单描述一下上述算法的流程。1~5行将每个节点的key值设置为∞(除根节点r以外,根节点key值置为0,作为第一个被处理的点),然后将每个结点的父节点置为NIL,并对最小优先队列Q进行初始化,使其包含图中的所有节点。

  算法维持的循环不变式由3个部分组成:

  ①A={(v,v.π):v∈V-{r}-Q}

  ②已经加入到最小生成树中的结点集合为V-Q

  ③对于所有结点v∈Q,如果v.π≠NIL,则v.key<∞并且v.key是连接结点v和最小生成树中某个节点的轻量级边(v,v.π)的权重

  第7行找出结点v∈Q,该结点是某条横跨切割<V-Q,Q>的轻量级边的一个端点,然后将结点u从队列中删除,并将其加入到集合V-Q中,也就是将边<u,u.π>加入到集合A中。8~11行的for循环将每个与u邻接但是不在树中的结点v的key和π属性进行更新,从而维持循环不变式。

  (3)简单看一个Prim算法的例子

  例子中,初始的根节点为a,加阴影的边和黑色结点属于树A。在算法的每一步,树中的结点就决定了图的一个切割,横跨该切割的一条轻量级边就被加入到树中。

  例如:在图(b),轻量级边有两条,<b,c>,<a,h>的权重都为8,所以可以选择两条边中的一条加入到树中

   技术分享图片       技术分享图片   

   技术分享图片     技术分享图片

   技术分享图片       技术分享图片

    技术分享图片      技术分享图片   

  技术分享图片

 














以上是关于最小生成树算法的主要内容,如果未能解决你的问题,请参考以下文章

最小生成树详解 prim+ kruskal代码模板

最小生成树及Prim算法及Kruskal算法的代码实现

图的最小生成树算法(图解+代码)| 学不会来看我系列

最小生成树

最小生成树算法:Kruskal算法 Prim算法

急!数据结构最小生成树prim算法C语言实现