根據樹的特性可知,連通圖的生成樹是圖的極小連通子圖,它包含圖中的所有頂點,但只有構成一棵樹的邊;生成樹又是圖的極大無迴路子圖,它的邊集是關聯圖中的全部頂點而又沒有造成迴路的邊。html
一個有n個頂點的連通圖的生成樹只有n-1條邊。如有n個頂點而少於n-1條邊,則是非連通圖(將其想成有n個頂點的一條鏈,則其爲連通圖的條件是至少有n-1條邊);若多於n-1條邊,則必定造成迴路。值得注意的是,有n-1條邊的生成子圖,並不必定是生成樹。此處,介紹一個概念。網:指的是邊帶有權值的圖。java
在一個網的全部生成樹中,權值總和最小的生成樹,稱之爲最小代價生成樹,也稱爲最小生成樹。node
根據生成樹的定義,具備n個頂點的連通圖的生成樹,有n個頂點和n-1條邊。所以,構造最小生成樹的準則有如下3條:算法
須要注意的一點是,儘管最小生成樹必定存在,但其並不必定是惟一的。如下介紹求圖的最小生成樹的兩個典型的算法,分別爲克魯斯卡爾算法(kruskal)和普里姆算法(prim)數據結構
克魯斯卡爾算法是根據邊的權值遞增的方式,依次找出權值最小的邊創建的最小生成樹,而且規定每次新增的邊,不能形成生成樹有迴路,直到找到n-1條邊爲止。ide
基本思想:設圖G=(V,E)是一個具備n個頂點的連通無向網,T=(V,TE)是圖的最小生成樹,其中V是T的頂點集,TE是T的邊集,則構造最小生成樹的具體步驟以下:測試
T的初始狀態爲T=(V,空集),即開始時,最小生成樹T是圖G的生成零圖ui
將圖G中的邊按照權值從小到大的順序依次選取,若選取的邊未使生成樹T造成迴路,則加入TE中,不然捨棄,直至TE中包含了n-1條邊爲止this
下圖演示克魯斯卡爾算法的構造最小生成樹的過程:spa
其示意代碼以下:
相關代碼:
package all_in_tree; import java.util.Comparator; import java.util.PriorityQueue; import java.util.Queue; import algorithm.PathCompressWeightQuick_Union; import algorithm.UF; /** * 該類用於演示克魯斯卡爾算法的過程 * @author 學徒 * *因爲每次添加一條邊時,須要判斷所添加的邊是否會產生迴路,而回路的產生,當且僅當邊上的兩個節點處在同一個連通 *分支上,爲此,可使用Union-Find算法來判斷邊上的兩個點是否處在同一個連通分支上 * */ public class Kruskal { //用於記錄節點的數目 private int nodeCount; //用於判斷是否會造成迴路 private UF unionFind; //用優先級隊列,每次最早出隊的是其權值最小的邊 private Queue<Edge> q; //用於存儲圖的生成樹 private Edge[] tree; /** * 初始化一個圖的最小生成樹所需的數據結構 * @param n 圖的節點的數目 */ public Kruskal(int n) { this.nodeCount=n; tree=new Edge[n-1]; unionFind=new PathCompressWeightQuick_Union(n); Comparator<Edge> cmp=new Comparator<Edge>() { @Override public int compare(Edge obj1,Edge obj2) { int obj1W=obj1.weight; int obj2W=obj2.weight; if(obj1W<obj2W) return -1; else if(obj1W>obj2W) return 1; else return 0; } }; q=new PriorityQueue<Edge>(11,cmp); } /** * 用於添加一條邊 * @param edge 所要進行添加的邊 */ public void addEdge(Edge edge) { q.add(edge); } /** * 用於生成最小生成樹 * @return 最小生成樹的邊集合 */ public Edge[] getTree() { //用於記錄加入圖的最小生成樹的邊的數目 int edgeCount=0; //用於獲得最小生成樹 while(!q.isEmpty()&&edgeCount<this.nodeCount-1) { //每次取出權值最小的一條邊 Edge e=q.poll(); //判斷是否產生迴路,當其不產生迴路時,將其加入到最小生成樹中 int index1=unionFind.find(e.node1); int index2=unionFind.find(e.node2); if(index1!=index2) { tree[edgeCount++]=e; unionFind.union(e.node1, e.node2); } } return tree; } } /** * 測試用例所使用的類,該類的測試用例即爲上圖中中所示的Kruskal算法最小生成樹的構造 * 過程的示例圖,且其節點編號從0開始,而不從1開始 * @author 學徒 * */ class Test { public static void main(String[] args) { Kruskal k=new Kruskal(6); k.addEdge(new Edge(0,3,5)); k.addEdge(new Edge(0,1,6)); k.addEdge(new Edge(1,4,3)); k.addEdge(new Edge(4,5,6)); k.addEdge(new Edge(3,5,2)); k.addEdge(new Edge(0,2,1)); k.addEdge(new Edge(1,2,5)); k.addEdge(new Edge(2,4,6)); k.addEdge(new Edge(2,5,4)); k.addEdge(new Edge(2,3,6)); Edge[] tree=k.getTree(); for(Edge e:tree) { System.out.println(e.node1+" --> "+e.node2+" : "+e.weight); } } } /** * 圖的邊的數據結構 * @author 學徒 * */ class Edge { //節點的編號 int node1; int node2; //邊上的權值 int weight; public Edge() { } public Edge(int node1,int node2,int weight) { this.node1=node1; this.node2=node2; this.weight=weight; } } 運行結果: 0 --> 2 : 1 3 --> 5 : 2 1 --> 4 : 3 2 --> 5 : 4 1 --> 2 : 5
ps:上述代碼中所用到的Union-Find算法的相關代碼及解析,請點擊 K:Union-Find(並查集)算法 進行查看
分析 :該算法的時間複雜度爲O(elge),即克魯斯卡爾算法的執行時間主要取決於圖的邊數e,爲此,該算法適用於針對稀疏圖的操做
爲描述的方便,在介紹普里姆算法前,給出以下有關距離的定義:
兩個頂點之間的距離:是指將頂點u鄰接到v的關聯邊的權值,即爲|u,v|。若兩個頂點之間無邊相連,則這兩個頂點之間的距離爲無窮大
頂點到頂點集合之間的距離:頂點u到頂點集合V之間的距離是指頂點u到頂點集合V中全部頂點之間的距離中的最小值,即爲|u,V|=$min|u,v| , v\in V$
兩個頂點集合之間的距離:頂點集合U到頂點集合V的距離是指頂點集合U到頂點集合V中全部頂點之間的距離中的最小值,記爲|U,V|=$min|u,V| , u\in U$
基本思想:假設G=(V,E)是一個具備n個頂點的連通網,T=(V,TE)是網G的最小生成樹。其中,V是R的頂點集,TE是T的邊集,則最小生成樹的構造過程以下:從U={u0},TE=$\varnothing$開始,必存在一條邊(u*,v*),u*$\in U$,v*$\in V-U$,使得|u*,v*|=|U,V-U|,將(u*,v*)加入集合TE中,同時將頂點v*加入頂點集U中,直到U=V爲止,此時,TE中必有n-1條邊(最小生成樹存在的狀況),最小生成樹T構造完畢。下圖演示了使用Prim算法構造最小生成樹的過程
其示意代碼以下:
相關代碼:
package all_in_tree; /** * 該類用於演示Prim算法構造最小生成樹的過程 * @author 學徒 * */ public class Prim { //用於記錄圖中節點的數目 private int nodeCount; //用於記錄圖的領接矩陣,其存儲對應邊之間的權值 private int[][] graph; //用於記錄其對應節點是否已加入集合U中,若加入了集合U中,則其值爲true private boolean[] inU; //用於記錄其生成的最小生成樹的邊的狀況 private Edge[] tree; //用於記錄其下標所對的節點的編號相對於集合U的最小權值邊的權值的狀況 private int[] min; //用於記錄其下標所對的節點的最小權值邊所對應的集合U中的節點的狀況 private int[] close; /** * 用於初始化 * @param n 節點的數目 */ public Prim(int n) { this.nodeCount=n; this.graph=new int[n][n]; //初始化的時候,將各點的權值初始化爲最大值 for(int i=0;i<n;i++) { for(int j=0;j<n;j++) { graph[i][j]=Integer.MAX_VALUE; } } this.inU=new boolean[n]; this.tree=new Edge[n-1]; this.min=new int[n]; this.close=new int[n]; } /** *用於爲圖添加一條邊 * @param edge 邊的封裝類 */ public void addEdge(Edge edge) { int node1=edge.node1; int node2=edge.node2; int weight=edge.weight; graph[node1][node2]=weight; graph[node2][node1]=weight; } /** * 用於獲取其圖對應的最小生成樹的結果 * @return 由最小生成樹組成的邊的集合 */ public Edge[] getTree() { //用於將第一個節點加入到集合U中 for(int i=1;i<nodeCount;i++) { min[i]=graph[0][i]; close[i]=0; } inU[0]=true; //用於循環n-1次,每次循環添加一條邊進最小生成樹中 for(int i=0;i<nodeCount-1;i++) { //用於記錄找到的相對於集合U中的節點的最小權值的節點編號 int node=0; //用於記錄其相對於集合U的節點的最小的權值 int mins=Integer.MAX_VALUE; //用於尋找其相對於集合U中最小權值的邊 for(int j=1;j<nodeCount;j++) { if(min[j]<mins&&!inU[j]) { mins=min[j]; node=j; } } //用於記錄其邊的狀況 tree[i]=new Edge(node,close[node],mins); //修改相關的狀態 inU[node]=true; //修改其相對於集合U的狀況 for(int j=1;j<nodeCount;j++) { if(!inU[j]&&graph[node][j]<min[j]) { min[j]=graph[node][j]; close[j]=node; } } } return tree; } } class Edge { //節點的編號 int node1; int node2; //邊上的權值 int weight; public Edge() { } public Edge(int node1,int node2,int weight) { this.node1=node1; this.node2=node2; this.weight=weight; } } /** * 測試用例所使用的類,該類的測試用例即爲上圖中中所示的Prim算法最小生成樹的構造 * 過程的示例圖,且其節點編號從0開始,而不從1開始 * @author 學徒 * */ class Test { public static void main(String[] args) { Prim k=new Prim(6); k.addEdge(new Edge(0,3,5)); k.addEdge(new Edge(0,1,6)); k.addEdge(new Edge(1,4,3)); k.addEdge(new Edge(4,5,6)); k.addEdge(new Edge(3,5,2)); k.addEdge(new Edge(0,2,1)); k.addEdge(new Edge(1,2,5)); k.addEdge(new Edge(2,4,6)); k.addEdge(new Edge(2,5,4)); k.addEdge(new Edge(2,3,5)); Edge[] tree=k.getTree(); for(Edge e:tree) { System.out.println(e.node1+" --> "+e.node2+" : "+e.weight); } } } 運行結果以下: 2 --> 0 : 1 5 --> 2 : 4 3 --> 5 : 2 1 --> 2 : 5 4 --> 1 : 3
總結:kruskal算法的時間複雜度與求解最小生成樹的圖中的邊數有關,而prim算法的時間複雜度與求解最小生成樹的圖中的節點的數目有關。爲此,Kruskal算法更加適用於稀疏圖,而prim算法適用於稠密圖。當e>=n^2時,kruskal算法比prim算法差,但當e=O(n^2)時,kruskal算法卻比prim算法好得多。