好久, 沒寫blog了,今天,多寫點。
上節說到那里了,是不是說圖的遍歷,這節,我們來通過圖的具體的應用了。
首先,看看最小生成樹的應用了。 什么是最小生成樹了?
由生成樹的定義可知,無向連通圖的生成樹不是唯一的,對連通圖的不同遍歷就得到不同的生成樹。圖 b 所示是圖 (a)所示的無向連通圖的部分生成樹。
所謂最小生成樹是 如果是一個無向連通網, 那么它的所有生成樹中必有一棵邊的權值總和最小的生成樹,我們稱這棵生成樹為最小代價生成樹(Minimum Cost Spanning Tree).
許多應用問題都是一個求無向連通網的最小生成樹問題。例如,要在 n個城市之間鋪設光纜, 鋪設光纜的費用很高, 並且各個城市之間鋪設光纜的費用不同。一個目標是要使這 n個城市的任意兩個之間都可以直接或間接通信, 另一個目標是要使鋪設光纜的總費用最低。如果把 n個城市看作是圖的 n個頂點,兩個城市之間鋪設的光纜看作是兩個頂點之間的邊, 這實際上就是求一個無向連通網的最小生成樹問題。
由最小生成樹的定義可知, 構造有 n個頂點的無向連通網的最小生成樹必須滿足以下三個條件:
(1)構造的最小生成樹必須包括 n個頂點;
(2)構造的最小生成樹有且僅有 n-1條邊;
(3)構造的最小生成樹中不存在回路。
構造最小生成樹的方法有許多種,典型的方法有兩種,一種是普里姆(Prim)算法,一種是克魯斯卡爾(Kruskal)算法。
一普里姆(Prim)算法
假設 G=(V,E)為一無向連通網,其中,V 為網中頂點的集合,E 為網中邊的集合。設置兩個新的集合 U 和 T,其中,U 為 G 的最小生成樹的頂點的集合,T 為 G 的最小生成樹的邊的集合。普里姆算法的思想是:令集合 U 的初值為 U={u1}(假設構造最小生成樹時從頂點 u1開始) ,集合 T 的初值為 T={}。從所有的頂點 u∈U 和頂點 v∈V-U 的帶權邊中選出具有最小權值的邊(u,v) ,將頂點 v 加入集合 U 中,將邊(u,v)加入集合 T 中。如此不斷地重復直到 U=V 時,最小生成樹構造完畢。此時,集合 U 中存放着最小生成樹的所有頂點,集合 T中存放着最小生成樹的所有邊。
以下圖(a)為例,說明用普里姆算法構造圖的無向連通網的最小生成樹的過程。
為了分析問題的方便,無向連通網如圖(a)所示。 初始時, 算法的集合 U={A}, 集合 V-U={B,C,D,E}, 集合 T={},如圖(b)所示。在所有 u 為集合 U 中頂點、v 為集合 V-U 中頂點的邊(u,v)中尋找具有最小權值的邊,尋找到的邊是(A,D),權值為 20,把頂點 B 加入到集合U 中, 把邊(A,D)加入到集合 T 中, 如圖 (c)所示。 在所有 u為集合 U 中頂點、v 為集合 V-U 中頂點的邊(u,v)中尋找具有最小權值的邊, 尋找到的邊是(D,E), 權值為 10,把頂點 E 加入到集合 U 中,把邊(D,E)加入到集合 T 中,如圖(d)所示。隨后依次從集合 V-U 中加入到集合 U 中的頂點為 B、C,依次加入到集合T中的邊為(A,B)(權值為 60) 、(E,C) (權值為 70) ,分別如圖 (e)、(f)所示。最后得到的圖(f)所示就是原無向連通網的最小生成樹。
本書以無向網的鄰接矩陣類 NetAdjMatrix<T>來實現普里姆算法。NetAdjMatrix<T>類的成員字段與無向圖鄰接矩陣類 GraphAdjMatrix<T>的成員字段一樣,不同的是,當兩個頂點間有邊相連接時,matirx 數組中相應元素的值是邊的權值,而不是 1
無向網鄰接矩陣類 NetAdjMatrix<T>源代碼的實現如下所示:
public class NetAdjMatrix<T> : IGraph<T>//繼承與圖形的接口 { private Node<T>[] nodes; //頂點數組 存取相應的結點的 泛型數組
private int numEdges; //邊的數目 上圖邊數字是6 private int[,] matrix; //鄰接矩陣數組 存取相應的互相的權值 //構造器 進行數據的初始化 邊的數目是0 public NetAdjMatrix (int n) { nodes = new Node<T>[n]; matrix = new int[n,n]; numEdges = 0; } //獲取索引為index的頂點的信息 算法的時間復雜度是O(1) public Node<T> GetNode(int index) { return nodes[index]; } //設置索引為index的頂點的信息 算法的時間復雜度是O(1)
public void SetNode(int index, Node<T> v)
{
nodes[index] = v;
}
//邊的數目屬性 可讀可寫的屬性
public int NumEdges
{
get
{
return numEdges;
}
set
{
numEdges = value;
}
}
//獲取matrix[index1, index2]的值 算法的時間復雜度是O(1)
public int GetMatrix(int index1, int index2)
{
return matrix[index1, index2];
}
//設置matrix[index1, index2]的值 算法的復雜度是O(1)
public void SetMatrix(int index1, int index2, int v)
{
matrix[index1, index2] = v;
}
//獲取頂點的數目 算法的時間的復雜度是O(1)
public int GetNumOfVertex()
{
return nodes.Length;
}
//獲取邊的數目 算法的時間的復雜度是O(1)
public int GetNumOfEdge()
{
return numEdges;
}
//v是否是無向網的頂點
//如果包含這個頂點 返回為真,否則返回為假。
//由於這是一層循環,算法的復雜度是O(n) public bool IsNode(Node<T> v) { //遍歷頂點數組 foreach (Node<T> nd in nodes) { //如果頂點nd與v相等,則v是圖的頂點,返回true if (v.Equals(nd)) { return true; } } return false; } //獲得頂點v在頂點數組中的索引
// 如果相等,返回相應的索引。
//由於是一層循環,時間的復雜度是O(n)
public int GetIndex(Node<T> v) { int i = -1; //遍歷頂點數組 for (i = 0; i < nodes.Length; ++i) { //如果頂點nd與v相等,則v是圖的頂點,返回索引值 if (nodes[i].Equals(v)) { return i; } } return i; } //在頂點v1、v2之間添加權值為v的邊
//添加相應的權值的v的邊, 這是一個對稱矩陣。
public void SetEdge(Node<T> v1, Node<T> v2, int v) { //v1或v2不是無向網的頂點 if (!IsNode(v1) || !IsNode(v2)) { Console.WriteLine("Node is not belong to Graph!"); return; } //矩陣是對稱矩陣 matrix[GetIndex(v1), GetIndex(v2)] = v; matrix[GetIndex(v2), GetIndex(v1)] = v; ++numEdges; } //刪除v1和v2之間的邊
// 刪除對稱矩陣。
public void DelEdge(Node<T> v1, Node<T> v2) { //v1或v2不是無向網的頂點 if (!IsNode(v1) || !IsNode(v2)) { Console.WriteLine("Node is not belong to Graph!"); return; } //v1和v2之間存在邊 if (matrix[GetIndex(v1), GetIndex(v2)] != int.MaxValue) { //矩陣是對稱矩陣 matrix[GetIndex(v1), GetIndex(v2)] = int.MaxValue; matrix[GetIndex(v2), GetIndex(v1)] = int.MaxValue; --numEdges; } } //判斷v1和v2之間是否存在邊
//判斷相應 不是 最大值 返回為真 否則 為假 算法的時間復雜度O(1) public bool IsEdge(Node<T> v1, Node<T> v2) { //v1或v2不是無向網的頂點 if (!IsNode(v1) || !IsNode(v2)) { Console.WriteLine("Node is not belong to Graph!"); return false; } //v1和v2之間存在邊 if (matrix[GetIndex(v1), GetIndex(v2)] != int.MaxValue) { return true; } Else //v1和v2之間不存在邊 { return false; } } }
具體如圖所示:
為實現普里姆算法, 需要設置兩個輔助一維數組 lowcost和 closevex, lowcost用來保存集合 V-U 中各頂點與集合 U 中各頂點構成的邊中具有最小權值的邊的權值;closevex 用來保存依附於該邊的在集合 U 中的頂點。假設初始狀態時,U={u1}(u1為出發的頂點) ,這時有 lowcost[0]=0,它表示頂點 u1已加入集合 U中。數組 lowcost 元素的值是頂點 u1 到其他頂點所構成的直接邊的權值。然后不斷選取權值最小的邊(ui,uk)(ui∈U,uk∈V-U),每選取一條邊,就將 lowcost[k]置為 0,表示頂點 uk 已加入集合 U 中。由於頂點 uk 從集合 V-U 進入集合 U 后,這兩個集合的內容發生了變化, 就需要依據具體情況更新數組lowcost和closevex中部分元素的值。把普里姆算法 PrimNetAdjMatrix<T>類的成員方法,實現的源代碼如下:
public int[] Prim() { int[] lowcost = new int[nodes.Length]; //權值數組 保存權值的數組 int[] closevex = new int[nodes.Length]; //頂點數組 保存 相應各個頂點的數組 int mincost = int.MaxValue; //最小權值 默認是 int的最大值 表示無窮大 //輔助數組初始化 對摸個 權值數組賦值 保存 最小值 for (int i = 1; i < nodes.Length; ++i) { lowcost[i] = matrix[0, i]; closevex[i] = 0; } //某個頂點加入集合U lowcost[0] = 0; closevex[0] = 0;
//判斷最小的權值通過的頂點的循環就此開始 for(int i=0; i<nodes.Length; ++i) { int k = 1; int j = 1; //選取權值最小的邊和相應的頂點 while(j < nodes.Length) { if (lowcost[j] < mincost && lowcost[j] != 0) { k = j; } ++j; } //新頂點加入集合U lowcost[k] = 0; //重新計算該頂點到其余頂點的邊的權值 for (j = 1; j < nodes.Length; ++j) { if (matrix[k, j] < lowcost[j]) { lowcost[j] = matrix[k, j]; closevex[j] = k; } } } return closevex;
}
//我們明顯的看出來,由於用到了雙重循環,其算法的時間的復雜度是O(n^2)
具體實現,如圖所示:
在普里姆算法中,第一個for循環的執行次數為n-1,第二個for循環中又包括了一個while循環和一個for循環,執行次數為 2(n-1)2,所以普里姆算法的時間復雜度為O(n2)。
這節,我們隊圖的引用做了一個拋磚引玉的介紹,主要是普利姆算法,解決 最小生成樹問題。下節,我們介紹一下克魯斯卡爾算法解決最小生成樹的問題,和其他的應用。對圖做一個總結等等。