1 場景描述
系統中用到了進度計划編制功能,支持從project文件直接導入數據,並能夠在系統中對wbs任務進行增、刪、改操作。wbs任務分解中一個重要的概念就是前置任務,前置任務設置確定了不同任務項之間的依賴關系,以軟件開發的一般過程為例,需求調研就是系統設計的前置任務。具體來說前置任務又分為以下四種類型
- Finish-to-Start (FS)
把這個任務的開始日期和前提條件任務的結束日期對齊,一般用於串行的任務安排,前一個任務必須完成后才能啟動下一個新任務
- Start-to-Start (SS)
把這個任務的開始日期和前提條件任務的開始日期對齊,一般用於並行任務的安排,也可以一個任務啟動后,第二個任務延后或提前數日啟動。
- Finish-to-Finish (FF)
把這個任務的結束日期和前提條件任務的結束日期對齊,可以用於協調任務的統一時間完成,這樣可以定義好任務的開始時間
- Start-to-Finish (SF)
把這個任務的結束日期和前提條件任務的開始日期對齊,或者說是前置任務開始的日期決定了后續任務的完成時間
不管是哪種類型,某項任務總是依賴於其前置任務,這就要求,任務的前置關系不能出現循環(閉環),比如A->B->A這種情況是絕對不允許的。
任務關系表基本數據格式如下
SourceId跟TargetId標識任務的Id,通過SourceId、TargetId確定任務之間前后置關系。每個任務項可以看作是一個節點,任務的前置關系可以標識節點與節點之間有向連線,這在數據結構中是一種標准的有向圖。
2 圖及圖的存儲結構
2.1 圖的基本概念
先看一下數據結構中對圖的定義:圖是由有窮、非空點集和邊集合組成,簡寫成G(V,E);
其中G表示Graph,V和E是圖中兩個基本元素,V表示Vertex(頂點),E表示Edge(邊)。圖按照邊是否有方向又分為有向圖和無向圖,上面我們看到用箭頭表示邊方向的是一個有向圖,無向圖一般用下圖方式表示。
本文還涉及到關於圖的一個重要概念是度。
度:與某個頂點相連接的邊數稱為該定點的度
出度、入度:對於有向圖的概念,出度表示此頂點為起點的邊的數目,入度表示此頂點為終點的邊的數目
2.2 圖的存儲結構
圖的存儲結構設計有很多種,常用的有鄰接矩陣和鄰接鏈表兩種。
2.2.1 鄰接矩陣
鄰接矩陣采用2個數組,一個1維數組用來存儲節點信息,一個2維數組用來存儲邊信息。其中二維數組arr[i][j]表示節點i到j的邊信息,如果為1表示有邊,如果為0表示無邊。
從這個矩陣中,很容易知道圖中的信息。
1、可以判斷任意兩頂點之間是否有邊無邊
2、要知道某個頂點的度,其實就是這個頂點vi在鄰接矩陣中第i行或(第i列)的元素之和
3、求頂點vi的所有鄰接點就是將矩陣中第i行元素掃描一遍,arc[i][j]為1就是鄰接點
2.2.2 鄰接鏈表
鄰接鏈表總體思路如下:
圖中頂點用一個一維數組存儲,當然,頂點也可以用單鏈表來存儲,不過,數組可以較容易的讀取頂點的信息,更加方便。
圖中每個頂點vi的所有鄰接點構成一個線性表,由於鄰接點的個數不定,所以,用單鏈表存儲,無向圖稱為頂點vi的邊表,有向圖則稱為頂點vi作為弧尾的出邊表。
頂點表的各個結點由data和firstedge兩個域表示,data是數據域,存儲頂點的信息,firstedge是指針域,指向邊表的第一個結點,即此頂點的第一個鄰接點。邊表結點由adjvex和next兩個域組成。adjvex是鄰接點域,存儲某頂點的鄰接點在頂點表中的下標,next則存儲指向邊表中下一個結點的指針。
關於圖的多種存儲結構設計方式,請參考數據結構相關數據,慢慢理解。
本文采用鄰接鏈表存儲結構實現,對於有向圖是否包含閉環的判斷,采用的是拓撲排序方法,如果能夠用拓撲排序完成對圖中所有節點的排序的話,就說明這個圖中沒有環,而如果不能完成,則說明有環。
拓撲排序算法的主要操作步驟如下:
1、從有向圖中選取一個沒有前驅(即入度為0)的頂點,並輸出之;
2、從有向圖中刪去此頂點同時找到該頂點的鄰接點,將該頂點的鄰接點的入度-1,若入度為0則壓入棧中
重復上述兩步,直至圖空,或者圖不空但找不到入度為0的頂點為止。如果找到的頂點數與圖的頂點集合總數相等,說明無閉環,否則說明存在閉環。具體實現思路還需要慢慢體會。
3 編碼實現
根據上面對圖的鄰接鏈表相關定義及理解,首先定義圖的頂點類。
/// <summary> /// 頂點 /// </summary> /// <typeparam name="TValue">數據類型泛型</typeparam> public class Vertex<TValue> { public TValue data; // 數據 public Node<TValue> firstLinkNode; // 第一個鄰接節點 public bool visited; // 訪問標志,遍歷時使用 public int inDegree; // 表示該節點入度 /// <summary> /// 構造函數 /// </summary> /// <param name="value"></param> public Vertex(TValue value) { data = value; } }
定義鏈表鄰接點類
/// <summary> /// 表示鏈表中的鄰接點 /// </summary> public class Node<TValue> { public Vertex<TValue> adjvex; //頂點 public Node<TValue> next; //下一個鄰接點 /// <summary> /// 構造函數 /// </summary> /// <param name="value"></param> public Node(Vertex<TValue> value) { adjvex = value; } }
定義鄰接鏈表表示類,其中包含圖的頂點集合的屬性、添加頂點、添加邊(有向邊、無向邊)、拓撲排序是否成功(有向圖閉環檢測)等操作方法,具體的實現及說明參看代碼注釋。
/// <summary> /// 圖的鄰接表表示類 /// </summary> /// <typeparam name="T">泛型類型</typeparam> public class AdjacencyList<T> { List<Vertex<T>> items; // 圖的頂點集合 /// <summary> /// 構造函數 /// </summary> public AdjacencyList() { items = new List<Vertex<T>>(); } /// <summary> /// 添加一個頂點 /// </summary> /// <param name="item"></param> public void AddVertex(T item) { // 頂點不存在 if (!Contains(item)) { items.Add(new Vertex<T>(item)); } } /// <summary> /// 添加無向邊 /// </summary> /// <param name="from">頭頂點</param> /// <param name="to">尾頂點</param> public void AddEdge(T from, T to) { Vertex<T> fromVer = Find(from); //找到起始頂點 if (fromVer == null) throw new ArgumentException("頭頂點並不存在!"); Vertex<T> toVer = Find(to); //找到結束頂點 if (toVer == null) throw new ArgumentException("尾頂點並不存在!"); //無向圖的兩個頂點都需記錄邊信息,有向圖只需記錄單邊信息 //即無相圖的邊其實就是兩個雙向的有向圖邊 AddDirectedEdge(fromVer, toVer); AddDirectedEdge(toVer, fromVer); } /// <summary> /// 查找圖中是否包含某項 /// </summary> /// <param name="item"></param> /// <returns></returns> public bool Contains(T data) { foreach (Vertex<T> v in items) { if (v.data.Equals(data)) return true; } return false; } /// <summary> /// 根據頂點數據查找頂點 /// </summary> /// <param name="data">數據</param> /// <returns></returns> public Vertex<T> Find(T data) { foreach (Vertex<T> v in items) { if (v.data.Equals(data)) return v; } return null; } /// <summary> /// 添加有向邊 /// </summary> /// <param name="fromVer">頭頂點</param> /// <param name="toVer">尾頂點</param> public void AddDirectedEdge(Vertex<T> fromVer, Vertex<T> toVer) { if (fromVer.firstLinkNode == null) //無鄰接點時,當前添加的尾頂點就是firstLinkNode { fromVer.firstLinkNode = new Node<T>(toVer); } else // 該頭頂點已經存在鄰接點,則找到該頭頂點鏈表最后一個Node,將toVer添加到鏈表末尾 { Node<T> tmp, node = fromVer.firstLinkNode; do { // 檢查是否添加了重復有向邊 if (node.adjvex.data.Equals(toVer.data)) { throw new ArgumentException("添加了重復的邊!"); } tmp = node; node = node.next; } while (node != null); tmp.next = new Node<T>(toVer); //添加到鏈表未尾 } } /// <summary> /// 拓撲排序是否能成功執行 /// 對有向圖來說,如果能夠用拓撲排序完成對圖中所有節點的排序的話,就說明這個圖中沒有環,而如果不能完成,則說明有環。 /// </summary> /// <returns></returns> public bool TopologicalSort() { Stack<Vertex<T>> stack = new Stack<Vertex<T>>(); // 定義棧 items.ForEach(it => // 循環頂點集合,將入度為0的頂點入棧 { if (it.inDegree == 0) stack.Push(it); //入度為0的頂點入棧 }); int count = 0; // 定義查找到的頂點總數 while (stack.Count > 0) { Vertex<T> t = stack.Pop(); // 出棧 count++; if (t.firstLinkNode != null) { Node<T> tmp = t.firstLinkNode; while (tmp != null) { tmp.adjvex.inDegree--; // 鄰接點入度-1 if (tmp.adjvex.inDegree == 0) // 如果鄰接點入度為0,則入棧 stack.Push(tmp.adjvex); tmp = tmp.next; // 遞歸所有鄰接點 } } } if (count < items.Count) // 找到的結果數量小於圖頂點個數相同,表示拓撲排序失敗,表示有閉環 { return false; } return true; } }
根據數據庫存儲的SourceId和TargetId集合,封裝一個GraphHelper類,提供一個檢測有向圖閉環的CheckDigraphLoop的靜態方法
/// <summary> /// 圖操作輔助類 /// </summary> public class GraphHelper { /// <summary> /// 檢測有向圖是否有閉環回路 /// </summary> /// <param name="originalData">初始數據:逗號分割的from跟to字符串集合</param> /// <returns></returns> public static bool CheckDigraphLoop(List<string> originalData) { AdjacencyList<string> adjacencyList = new AdjacencyList<string>(); string fromData = string.Empty; string toData = string.Empty; //構造有向圖的鄰接表表示 originalData.ForEach(it => { fromData = it.Split(',')[0]; //得到from頂點數據 toData = it.Split(',')[1]; //得到to定點數據 adjacencyList.AddVertex(fromData); adjacencyList.AddVertex(toData); var fromVertex = adjacencyList.Find(fromData); // 找到起始頂點 var toVertex = adjacencyList.Find(toData); // 找到目標頂點 toVertex.inDegree++; //目標頂點的入度+1 adjacencyList.AddDirectedEdge(fromVertex, toVertex); //添加有向邊 }); return adjacencyList.TopologicalSort(); } }
測試
static void Main(string[] args) { List<string> temp = new List<string>(); temp.Add("1,2"); temp.Add("1,3"); temp.Add("2,4"); temp.Add("2,5"); temp.Add("3,6"); temp.Add("3,7"); temp.Add("5,6"); temp.Add("6,1"); var result= GraphHelper.CheckDigraphLoop(temp); Console.WriteLine(result); Console.ReadLine(); }
4 總結
參考數據結構關於圖的相關C語言實現,用C#實現了通過拓撲排序算法進行的有向圖閉環檢測功能。
對於無向圖的閉環檢測檢測一般采用如下思路:
第一步:刪除所有度<=1的頂點及相關的邊,並將另外與這些邊相關的其它頂點的度減一。
第二步:將度數變為1的頂點排入隊列,並從該隊列中取出一個頂點重復步驟一。
如果最后還有未刪除頂點,則存在環,否則沒有環。
感興趣的朋友可以自己去揣摩實現一下。