圖的“多對多”特性使得圖在結構設計和算法實現上較為困難,這時就需要根據具體應用將圖轉換為不同的樹來簡化問題的求解。
一、生成樹與最小生成樹
1.1 生成樹
對於一個無向圖,含有連通圖全部頂點的一個極小連通子圖成為生成樹(Spanning Tree)。其本質就是從連通圖任一頂點出發進行遍歷操作所經過的邊,再加上所有頂點構成的子圖。
采用深度優先遍歷獲得的生成樹稱為深度優先生成樹(DFS生成樹),采用廣度優先遍歷獲得的生成樹稱為廣度優先生成樹(BFS生成樹)。如下圖所示,無向圖的DFS生成樹和BFS生成樹分別如圖的中間和右邊所示。
1.2 最小生成樹
如果連通圖是一個帶權的網絡,稱該網絡的所有生成樹中權值綜合最小的生成樹為最小生成樹(Minimum Spanning Tree,MST),簡稱MST生成樹。
求網絡的最小生成樹的重要意義就在於:假如要在n個城市之間鋪設光纜,由於地理環境的不同,各個城市之間鋪設光纜的費用也不同。一方面要使得這n個城市可以直接或間接通信,另一方面要考慮鋪設光纜的費用最低。解決這個問題的方法就是在n個頂點(城市)和不同權值的邊(這里指鋪設光纜的費用)所構成的無向連通圖中找出最小生成樹。
二、Prim算法
2.1 算法思想
假設N=(V,{E})是連通網,TE是N上最小生成樹中邊的集合。算法從U={u0}(u0∈V),TE={}開始。重復執行下述操作:在所有u∈U,v∈V-U的邊(u,v)∈E中找一條代價最小的邊(u0,v0)並入集合TE,同時v並入U,直至U=V為止。此時TE中必有n-1條邊,則T=(V,{TE})為N的最小生成樹。
2.2 算法實現
下面實現的Prim算法主要基於鄰接矩陣來構建:
#region Test03:最小生成樹算法之Prim算法測試:基於鄰接矩陣 static void PrimTest() { int[,] cost = new int[6, 6]; // 模擬圖的鄰接矩陣初始化 cost[0, 1] = cost[1, 0] = 6; cost[0, 2] = cost[2, 0] = 6; cost[0, 3] = cost[3, 0] = 1; cost[1, 3] = cost[3, 1] = 5; cost[2, 3] = cost[3, 2] = 5; cost[1, 4] = cost[4, 1] = 3; cost[3, 4] = cost[4, 3] = 6; cost[3, 5] = cost[5, 3] = 4; cost[4, 5] = cost[5, 4] = 6; cost[2, 5] = cost[5, 2] = 2; // Prim算法構造最小生成樹:從頂點0開始 Console.WriteLine("Prim算法構造最小生成樹:(從頂點0開始)"); double sum = 0; Prim(cost, 0, ref sum); Console.WriteLine("最小生成樹權值和為:{0}", sum); } static void Prim(int[,] V, int vertex, ref double sum) { int length = V.GetLength(1); // 獲取元素個數 int[] lowcost = new int[length]; // 待選邊的權值集合V int[] U = new int[length]; // 最終生成樹值集合U for (int i = 0; i < length; i++) { lowcost[i] = V[vertex, i]; // 將鄰接矩陣起始點矩陣中所在行的值加入V U[i] = vertex; // U集合中的值全為起始頂點 } lowcost[vertex] = -1; // 起始節點標記為已使用:-1代表已使用,后續不再判斷 for (int i = 1; i < length; i++) { int k = 0; // k標識V集合中最小值索引 int min = int.MaxValue; // 輔助變量:記錄最小權值 // 下面for循環中尋找V集合中權值最小的與頂點i鄰接的頂點j for (int j = 0; j < length; j++) { if (lowcost[j] > 0 && lowcost[j] < min) // 尋找范圍不包括0、-1以及無窮大值 { min = lowcost[j]; k = j; // k記錄最小權值索引 } } // 找到了並進行打印輸出 Console.WriteLine("找到邊({0},{1})權為:{2}", U[k], k, min); lowcost[k] = -1; // 標志為已使用 sum += min; // 累加權值 for (int j = 0; j < length; j++) { // 如果集合U中有多個頂點與集合V中某一頂點存在邊 // 則選取最小權值邊加入lowcost集合中 if (V[k, j] != 0 && (lowcost[j] == 0 || V[k, j] < lowcost[j])) { lowcost[j] = V[k, j]; // 更新集合lowcost U[j] = k; // 更新集合U } } } } #endregion
運行結果如下圖所示,最小生成樹的權值和為15:
Summary:Prim算法主要是對圖的頂點進行操作,它適用於稠密圖。
三、Kruskal算法
3.1 算法思想
Kruskal算法是一種按權值的遞增順序來選擇合適的邊來構造最小生成樹的方法。假設N=(V,{E})是連通網,則令最小生成樹的初始狀態為只有n個頂點而無邊的非連通圖T={V,{}},圖中每個頂點自成一個連通分量。在E中選擇代價最小的邊,若該邊依附的頂點落在T中不同的連通分量上,則將此邊加入到T中,否則舍去此邊而選擇下一條代價最小的邊。依次類推,直至T中所有頂點都在同一連通分量上為止。
下面是一個Kruskal算法的演示:
3.2 算法實現
(1)存放邊信息的結構體:記錄邊的起點、終點以及權值
struct Edge : IComparable { public int Begin; // 邊的起點 public int End; // 邊的終點 public int Weight; // 邊的權值 public Edge(int begin, int end, int weight) { this.Begin = begin; this.End = end; this.Weight = weight; } public int CompareTo(object obj) { Edge edge = (Edge)obj; return this.Weight.CompareTo(edge.Weight); } }
(2)創建按權值排序的邊的集合
static List<Edge> BuildEdgeList(int[,] cost) { int length = cost.GetLength(1); List<Edge> edgeList = new List<Edge>(); // 邊集合 for (int i = 0; i < length - 1; i++) { for (int j = i + 1; j < length; j++) { if (cost[i, j] > 0) { if (i < j) // 將序號較小的頂點放在前面 { Edge newEdge = new Edge(i, j, cost[i, j]); edgeList.Add(newEdge); } else { Edge newEdge = new Edge(j, i, cost[i, j]); edgeList.Add(newEdge); } } } } edgeList.Sort(); // 讓各邊按權值從小到大排序 return edgeList; }
(3)Kruskal核心代碼:Kruskal的實現過程是將森林變成樹的過程,可以給森林中的每一棵樹配置一個唯一的分組號,當兩棵樹合並為一棵樹后,使它們具有相同的分組號。因此,在樹合並時,如果兩棵樹具有相同的分組號,表明合並后的樹存在回路。
static void Kruskal(int[,] cost, int vertex, ref double sum) { int length = cost.GetLength(1); List<Edge> edgeList = BuildEdgeList(cost); // 獲取邊的有序集合 int[] groups = new int[length]; // 存放分組號的輔助數組 for (int i = 0; i < length; i++) { // 輔助數組的初始化:每個頂點配置一個唯一的分組號 groups[i] = i; } for (int k = 1, j = 0; k < length; j++) { int begin = edgeList[j].Begin; // 邊的起始頂點 int end = edgeList[j].End; // 邊的結束頂點 int groupBegin = groups[begin]; // 起始頂點所屬分組號 int groupEnd = groups[end]; // 結束頂點所屬分組號 // 判斷是否存在回路:通過分組號來判斷->不是一個分組即不存在回路 if (groupBegin != groupEnd) { // 打印最小生成樹邊的信息 Console.WriteLine("找到邊({0},{1})權值為:{2}", begin, end, edgeList[j].Weight); sum += edgeList[j].Weight; // 權值之和累加 k++; for (int i = 0; i < length; i++) { // 兩棵樹合並為一課后,將樹的所有頂點所屬分組號設為一致 if (groups[i] == groupEnd) { groups[i] = groupBegin; } } } } }
Summary:Kruskal算法主要針對圖的邊進行操作,因此它適用於稀疏圖。
參考資料
(1)程傑,《大話數據結構》
(2)陳廣,《數據結構(C#語言描述)》
(3)段恩澤,《數據結構(C#語言版)》