WBS任務分解中前置任務閉環迴路檢測:有向圖的簡單應用(C#)

1 場景描述

系統中用到了進度計劃編制功能,支持從project文件直接導入數據,並可以在系統中對wbs任務進行增、刪、改操做。wbs任務分解中一個重要的概念就是前置任務,前置任務設置肯定了不一樣任務項之間的依賴關係,以軟件開發的通常過程爲例,需求調研就是系統設計的前置任務。具體來講前置任務又分爲如下四種類型node

  • Finish-to-Start (FS)

   把這個任務的開始日期和前提條件任務的結束日期對齊,通常用於串行的任務安排,前一個任務必須完成後才能啓動下一個新任務算法

  • Start-to-Start (SS)

   把這個任務的開始日期和前提條件任務的開始日期對齊,通常用於並行任務的安排,也能夠一個任務啓動後,第二個任務延後或提早很多天啓動。數據庫

  • Finish-to-Finish (FF)

   把這個任務的結束日期和前提條件任務的結束日期對齊,能夠用於協調任務的統一時間完成,這樣能夠定義好任務的開始時間數組

  • Start-to-Finish (SF)

   把這個任務的結束日期和前提條件任務的開始日期對齊,或者說是前置任務開始的日期決定了後續任務的完成時間數據結構

不論是哪一種類型,某項任務老是依賴於其前置任務,這就要求,任務的前置關係不能出現循環(閉環),好比A->B->A這種狀況是絕對不容許的。函數

1

任務關係表基本數據格式以下oop

1

SourceId跟TargetId標識任務的Id,經過SourceId、TargetId肯定任務之間先後置關係。每一個任務項能夠看做是一個節點,任務的前置關係能夠標識節點與節點之間有向連線,這在數據結構中是一種標準的有向圖。測試

1

2 圖及圖的存儲結構

2.1 圖的基本概念

先看一下數據結構中對圖的定義:圖是由有窮、非空點集和邊集合組成,簡寫成G(V,E);編碼

其中G表示Graph,V和E是圖中兩個基本元素,V表示Vertex(頂點),E表示Edge(邊)。圖按照邊是否有方向又分爲有向圖和無向圖,上面咱們看到用箭頭表示邊方向的是一個有向圖,無向圖通常用下圖方式表示。spa

1

本文還涉及到關於圖的一個重要概念是

:與某個頂點相鏈接的邊數稱爲該定點的度

出度、入度:對於有向圖的概念,出度表示此頂點爲起點的邊的數目,入度表示此頂點爲終點的邊的數目

2.2 圖的存儲結構

圖的存儲結構設計有不少種,經常使用的有鄰接矩陣和鄰接鏈表兩種。

2.2.1 鄰接矩陣

鄰接矩陣採用2個數組,一個1維數組用來存儲節點信息,一個2維數組用來存儲邊信息。其中二維數組arr[i][j]表示節點i到j的邊信息,若是爲1表示有邊,若是爲0表示無邊。

 

1

從這個矩陣中,很容易知道圖中的信息。
一、能夠判斷任意兩頂點之間是否有邊無邊
二、要知道某個頂點的度,其實就是這個頂點vi在鄰接矩陣中第i行或(第i列)的元素之和
三、求頂點vi的全部鄰接點就是將矩陣中第i行元素掃描一遍,arc[i][j]爲1就是鄰接點

2.2.2 鄰接鏈表

鄰接鏈表整體思路以下:
圖中頂點用一個一維數組存儲,固然,頂點也能夠用單鏈表來存儲,不過,數組能夠較容易的讀取頂點的信息,更加方便。
圖中每一個頂點vi的全部鄰接點構成一個線性表,因爲鄰接點的個數不定,因此,用單鏈表存儲,無向圖稱爲頂點vi的邊表,有向圖則稱爲頂點vi做爲弧尾的出邊表。
頂點表的各個結點由data和firstedge兩個域表示,data是數據域,存儲頂點的信息,firstedge是指針域,指向邊表的第一個結點,即此頂點的第一個鄰接點。邊表結點由adjvex和next兩個域組成。adjvex是鄰接點域,存儲某頂點的鄰接點在頂點表中的下標,next則存儲指向邊表中下一個結點的指針。

2

關於圖的多種存儲結構設計方式,請參考數據結構相關數據,慢慢理解。

本文采用鄰接鏈表存儲結構實現,對於有向圖是否包含閉環的判斷,採用的是拓撲排序方法,若是可以用拓撲排序完成對圖中全部節點的排序的話,就說明這個圖中沒有環,而若是不能完成,則說明有環。

拓撲排序算法的主要操做步驟以下:

一、從有向圖中選取一個沒有前驅(即入度爲0)的頂點,並輸出之;

二、從有向圖中刪去此頂點同時找到該頂點的鄰接點,將該頂點的鄰接點的入度-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的頂點排入隊列,並從該隊列中取出一個頂點重複步驟一。  

若是最後還有未刪除頂點,則存在環,不然沒有環。 

感興趣的朋友能夠本身去揣摩實現一下。

相關文章
相關標籤/搜索