在有向圖中,邊是單向的:每條邊鏈接的兩個頂點都是一個有序對,它們的鄰接性是單向的。許多應用都是自然的有向圖,以下圖。爲實現添加這種單向性的限制很容易也很天然,看起來沒什麼壞處。但實際上這種組合性的結構對算法有深入的影響,使得有向圖和無向圖的處理大有不一樣。html
1.術語算法
雖然咱們爲有向圖的定義和無向圖幾乎相同(將使用的部分算法和代碼也是),但爲了說明邊的方向性而產生的細小文字差別所表明的結構特性是重點。數組
定義:一幅有方向性的圖(或有向圖)是由一組頂點和一組有方向的邊組成的,每條有方向的邊都連着有序的一對頂點。數據結構
咱們稱一條有向邊由第一個頂點指出並指向第二個頂點。在一幅有向圖中,一個頂點的出度爲由該頂點指出的邊的總數;一個頂點的入度爲指向該頂點的邊的總數。一條有向邊的第一個頂點稱爲它的頭,第二個頂點則稱爲它的尾。用 v->w 表示有向圖中一條由v 指向 w 的邊。一幅有向圖的兩個頂點的關係可能有四種:沒有邊相連;v->w; w-> v;v->w 和 w->v。閉包
在一幅有向圖中,有向路徑由一系列頂點組成,對於其中的每一個頂點都存在一條有向邊從它指向序列中的下一個頂點。有向環爲一條至少含有一條邊且起點和終點相同的有向路徑。路徑或環的長度即爲其中所包含的邊數。函數
當存在從 v 到 w 的有向路徑時,稱頂點 w 可以由頂點 v 達到。咱們須要理解有向圖中的可達性和無向圖中的連通性的區別。post
2.有向圖的數據類型性能
有向圖APIthis
有向圖表示spa
咱們使用鄰接表來表示有向圖,其中邊 v -> w 表示頂點 v 所對應的鄰接鏈表中包含一個 w 頂點。這種表示方法和無向圖幾乎相同並且更明晰,由於每條邊都只會出現一次。
有向圖取反
Digraph 的 API 中還添加了一個 Reverse 方法。它返回該有向圖的一個副本,但將其中全部邊的方向反轉。在處理有向圖時這個方法有時頗有用,由於這樣用例就能夠找出「指向」每一個頂點的全部邊,而 Adj 方法給出的是由每一個頂點指出的邊所鏈接的全部頂點。
頂點的符號名
在有向圖中,使用符號名做爲頂點也很簡單,參考 SymbolGraph。
namespace Digraphs { public class Digraph { private int v; private int e; private List<int>[] adj; public Digraph(int V) { this.v = V; this.e = 0; adj = new List<int>[v]; for (var i = 0; i < v; i++) { adj[i] = new List<int>(); } } public int V() { return v; } public int E() { return e; } public List<int> Adj(int v) { return adj[v]; } public void AddEdge(int v, int w) { adj[v].Add(w); e++; } public Digraph Reverse() { Digraph R = new Digraph(v); for (var i = 0; i < v; i++) foreach (var w in Adj(i)) R.AddEdge(w,i); return R; } } }
3.有向圖的可達性
在無向圖中介紹的深度優先搜索 DepthFirstSearch ,解決了單點連通性的問題,使得用例能夠斷定其餘頂點和給定的起點是否連通。使用徹底相同的代碼,將其中的 Graph 替換成 Digraph 也能夠解決有向圖中的單點可達性問題(給定一幅有向圖和一個起點 s ,是否存在一條從 s 到達給定頂點 v 的有向路徑?)。
在添加了一個接受多個頂點的構造函數以後,這份 API 使得用例可以解決一個更加通常的問題 -- 多點可達性 (給定一幅有向圖和頂點的集合,是否存在一條從集合中的任意頂點到達給定頂點 v 的有向路徑?)
下面的 DirectedDFS 算法使用瞭解決圖處理的標準範例和標準的深度優先搜索來解決。對每一個起點調用遞歸方法 Dfs ,以標記遇到的任意頂點。
namespace Digraphs { public class DirectedDFS { private bool[] marked; public DirectedDFS(Digraph G, int s) { marked = new bool[G.V()]; Dfs(G,s); } public DirectedDFS(Digraph G, IEnumerable<int> sources) { marked = new bool[G.V()]; foreach (var s in sources) { if (!marked[s]) Dfs(G,s); } } private void Dfs(Digraph G, int V) { marked[V] = true; foreach (var w in G.Adj(V)) { if (!marked[w]) Dfs(G,w); } } public bool Marked(int v) { return marked[v]; } } }
在有向圖中,深度優先搜索標記由一個集合的頂點可達的全部頂點所需的時間與被標記的全部頂點的出度之和成正比。
有向圖的尋路
在無向圖中的尋找路徑的算法,只需將 Graph 替換爲 Digraph 就可以解決下面問題:
1.單點有向路徑:給定一幅有向圖和一個起點 s ,從 s 到給定目的頂點是否存在一條有向路徑?若是有,找出這條路徑。
2.單點最短有向路徑:給定一幅有向圖和一個起點 s ,從 s 到給定目的頂點 v 是否存在一條有向路徑?若是有,找出其中最短的那條(所含邊數最少)。
4.環和有向無環圖
在和有向圖相關的實際應用中,有向環特別的重要。沒有計算機的幫助,在一幅普通的有向圖中找出有向環可能會很困難。從原則上來講,一幅有向圖可能含有大量的環;在實際應用中,咱們通常只重點關注其中一小部分,或者只想知道它們是否存在。
調度問題
一種應用普遍的模型是給定一組任務並安排它們的執行順序,限制條件是這些任務的執行方法和開始時間。限制條件還可能包括任務的耗時以及消耗的資源。最重要的一種限制條件叫作優先級限制,它指明瞭哪些任務必須在哪些任務以前完成。不一樣類型的限制條件會產生不一樣類型不一樣難度的調度問題。
下面以一個正在安排課程的大學生爲例,有些課程是其餘課程的先導課程:
若是假設該學生一次只能修一門課程,就會遇到優先級下的調度問題:給定一組須要完成的任務,以及一組關於任務完成的前後次序的優先級限制。在知足限制條件的前提下應該如何安排並完成全部任務?
對於任意一個這樣的問題,咱們先畫出一幅有向圖,其中頂點對應任務,有向邊對應優先級順序。爲了簡化問題,咱們以整數爲頂點:
在有向圖中,優先級限制下的調度問題等價於一個基本問題--拓撲排序:給定一幅圖,將全部頂點排序,使得全部的有向邊均從排在前面的元素指向排在後面的元素(或者說明沒法作到這一點)。
如圖,全部的邊都是向下的,因此清晰地表示了這幅有向圖模型所表明的有優先級限制的調度問題的一個解決方法:按照這個順序,該同窗能夠知足先導課程限制的條件下修完全部課程。
有向圖中的環
若是任務 x 必須在任務 y 以前完成,而任務 y 必須在任務 z 以前完成,但任務 z 又必須在任務 x 以前完成,那確定是有人搞錯了,由於這三個限制條件是不可能被同時知足的。通常來講,若是一個優先級限制的問題中存在有向環,那麼這個問題確定是無解的。要檢查這種錯誤,須要解決 有向環檢測:給定的有向圖中包含有向環嗎?若是有,按照路徑的方向從某個頂點並返回本身來找到環上的全部頂點。
一幅有向圖中含有環的數量多是圖的大小的指數級別,所以咱們只需找到一個環便可,而不是全部環。在任務調度和其餘許多實際問題中不容許出現有向環,所以有向無環圖就變得很特殊。
基於深度優先搜索能夠解決有向環檢測的問題,由於由系統維護的遞歸調用的棧表示的正是「當前」正在遍歷的有向路徑。一旦咱們找到了一條有向邊 v -> w 且 w 已經存在於棧中,就找到了一個環,由於棧表示的是一條由 w 到 v 的有向路徑,而 v -> w 正好補全了這個環。若是沒有找到這樣的邊,就意味着這副有向圖是無環的。DirectedCycle 基於這個思想實現的:
namespace Digraphs { public class DirectedCycle { private bool[] marked; private int[] edgeTo; private Stack<int> cycle;//有向環中的全部頂點(若是存在) private bool[] onStack;//遞歸調用的棧上的全部頂點 public DirectedCycle(Digraph G) { onStack = new bool[G.V()]; edgeTo = new int[G.V()]; marked = new bool[G.V()]; for (int v = 0; v < G.V(); v++) { if (!marked[v]) Dfs(G,v); } } private void Dfs(Digraph G, int v) { onStack[v] = true; marked[v] = true; foreach (var w in G.Adj(v)) { if (hasCycle()) return; else if (!marked[w]) { edgeTo[w] = v; Dfs(G, w); } else if (onStack[w]) { cycle = new Stack<int>(); for (int x = v; x != w; x = edgeTo[x]) cycle.Push(x); cycle.Push(w); cycle.Push(v); } } onStack[v] = false; } private bool hasCycle() { return cycle != null; } public IEnumerable<int> Cycle() { return cycle; } } }
該類爲標準的的遞歸 Dfs 方法添加了一個布爾類型的數組 onStack 來保存遞歸調用期間棧上的全部頂點。當它找到一條邊 v -> w 且 w 在棧中時,它就找到了一個有向環。環上的全部頂點能夠經過 edgeTo 中的連接獲得。
在執行 Dfs 時,查找的是一條由起點到 v 的有向路徑。要保存這條路徑,DirectedCycle 維護了一個 由頂點索引的數組 onStack,以標記遞歸調用的棧上的全部頂點(在調用 Dfs 時將 onStack[ v ] 設爲 true,在調用結束時將其設爲 false)。DirectedCycle 同時也使用了一個 edgeTo 數組,在找到有向環時返回環中的全部頂點。
頂點的深度優先次序與拓撲排序
優先級限制下的調度問題等價於計算有向無環圖中的全部頂點的拓撲排序:
下面算法的基本思想是深度優先搜索正好只會訪問每一個頂點一次。若是將 Dfs 的參數頂點保存在一個數據結構中,遍歷這個數據結構實際上就能訪問圖中的全部頂點,遍歷的順序取決於這個數據結構的性質以及是在遞歸調用以前仍是以後進行保存。在典型的應用中,頂點一下三種排列順序:
前序:在遞歸調用以前將頂點加入隊列;
後序:在遞歸調用以後將頂點加入隊列;
逆後序:在遞歸調用以後將頂點壓入棧。
該類容許用例用各類順序遍歷深度優先搜索通過得頂點。這在高級得有向圖處理算法很是有用,由於搜索得遞歸性使得咱們可以證實這段計算得許多性質。
namespace Digraphs { public class DepthFirstOrder { private bool[] marked; private Queue<int> pre;//全部頂點的前序排列 private Queue<int> post;//全部頂點的後序排列 private Stack<int> reversePost;//全部頂點的逆後序排列 public DepthFirstOrder(Digraph G) { marked = new bool[G.V()]; pre = new Queue<int>(); post = new Queue<int>(); reversePost = new Stack<int>(); for (var v = 0; v < G.V(); v++) { if (!marked[v]) Dfs(G,v); } } private void Dfs(Digraph G, int v) { pre.Enqueue(v); marked[v] = true; foreach (var w in G.Adj(v)) { if (!marked[w]) Dfs(G,w); } post.Enqueue(v); reversePost.Push(v); } public IEnumerable<int> Pre() { return pre; } public IEnumerable<int> Post() { return post; } public IEnumerable<int> ReversePost() { return reversePost; } } }
一幅有向無環圖得拓撲排序即爲全部頂點的逆後序排列。
拓撲排序
namespace Digraphs { public class Topological { private IEnumerable<int> order; public Topological(Digraph G) { DirectedCycle cycleFinder = new DirectedCycle(G); if (cycleFinder.HasCycle()) { DepthFirstOrder dfs = new DepthFirstOrder(G); order = dfs.ReversePost(); } } public IEnumerable<int> Order() { return order; } public bool IsDAG() { return order != null; } } }
這段使用 DirectedCycle 檢測是否有環,使用 DepthFirstOrder 返回有向圖的逆後序。
使用深度優先搜索對有向無環圖進行拓撲排序所需的時間和 V+E 成正比。第一遍深度優先搜索保證了不存在有向環,第二遍深度優先搜索產生了頂點的逆後序排列。
在實際應用中,拓撲排序和有向環的檢測老是一塊兒出現,由於有向環的檢測是排序的前提。例如,在一個任務調度應用中,不管計劃如何安排,其背後的有向圖中包含的環意味着存在一個必須被糾正的嚴重錯誤。所以,解決任務調度類應用一般須要一下3步:
1.指明任務和優先級條件;
2.不斷檢測並去除有向圖中的全部環,以確保存在可行方案;
3.使用拓撲排序解決調度問題。
相似地,調度方案的任何變更以後都須要再次檢查是否存在環,而後再計算新的調度安排。
5.有向圖中的強連通性
若是兩個頂點 v 和 w 是相互可達的,則稱它們爲強連通的。也就是說,即存在一條從 v 到 w 的有向路徑,也存在一條從 w 到 v 的有向路徑。若是一幅有向圖中的任意兩個頂點都是強連通的,則稱這副有向圖也是強連通的。
下面是強連通圖的例子,能夠看到,環在強連通性的理解上起着重要的做用。
強連通份量
和無向圖中的連通性同樣,有向圖中的強連通性也是一種頂點之間的等價關係:
自反性:任意頂點 v 和本身都是強連通的。
對稱性:若是 v 和 w 是強連通的,那麼 w 和 v 也是。
傳遞性:若是 v 和 w 是強連通的且 w 和 x 也是強連通的,那麼 v 和 x 也是強連通的。
做爲一種等價關係,強連通性將全部頂點分爲了一些等價類,每一個等價類都是由相互均爲強連通的頂點的最大子集組成。咱們稱這些子集爲強連通份量。以下圖,一個含有 V 個頂點的有向圖含有 1~ V個強連通份量——一個強連通圖只含有一個強連通份量,而一個有向無環圖則含有 V 個強連通份量。須要注意的是強連通份量的定義是基於頂點的,而不是邊。有些邊鏈接的兩個頂點都在同一個強連通份量中,而有些邊鏈接的兩個頂點則不在同一強連通份量中。
強連通份量API
設計一種平方級別的算法來計算強連通份量並不困難,單對於處理實際應用中的大型圖來講,平方級別的時間和空間需求是不可接受的。
Kosaraju算法
在有向圖中如何高效地計算強連通份量?咱們只需修改無向圖連通份量的算法 CC ,KosarajuCC 算法以下,它將會完成一下任務:
1.在給定的一幅有向圖 G 中,使用 DepthFirstOrder 來計算它的反向圖 GR 的逆後序排列;
2.在 G 中進行標準的深度優先搜索,可是要按照剛纔計算獲得的順序而非標準的順序來訪問全部未被標記的頂點;
3.在構造函數中,全部在同一個遞歸 Dfs() 調用中被訪問到的頂點都在同一個強連通份量中,將它們按照和 CC 相同的方式識別出來。
namespace Digraphs { public class KosarajuCC { private bool[] marked;//已訪問的頂點 private int[] id;//強連通份量的標識符 private int count;//強連通份量的數量 public KosarajuCC(Digraph G) { marked = new bool[G.V()]; id = new int[G.V()]; DepthFirstOrder order = new DepthFirstOrder(G.Reverse()); foreach (var s in order.ReversePost()) { if (!marked[s]) { Dfs(G,s); count++; } } } private void Dfs(Digraph G, int v) { marked[v] = true; id[v] = count; foreach (var w in G.Adj(v)) { if (!marked[w]) Dfs(G,w); } } public bool StronglyConnected(int v, int w) { return id[v] == id[w]; } public int Id(int v) { return id[v]; } public int Count() { return count; } } }
Kosaraju 算法的預處理所需的時間和空間與 V+E 成正比且支持常數時間的有向圖強連通性的查詢。
再談可達性
在無向圖中若是兩個頂點 V 和 W 是連通的,那麼就既存在一條從 v 到 w 的路徑也存在一條從 w 到 v 的路徑。在有向圖中若是兩個頂點 v 和 w 是強連通的,那麼就既存在一條從 v 到 w 的路徑也存在另外一條從 w 到 v 的路徑。但對於一對非強連通的頂點,也許存在一條從 v 到 w 的路徑,也許存在一條從 w 到 v 的路徑,也許兩條都不存在,但不可能兩條都存在。
頂點對的可達性:對於無向圖,等價於連通性問題;對於有向圖,它和強連通性有很大區別。 CC 實現須要線性級別的預處理時間才能支持常數時間的操做。在有向圖的相應實現中可否達到這樣的性能?
有向圖 G 的傳遞閉包是由相同的一組頂點組成的另外一幅有向圖,在傳遞閉包中存在一條從 v 指向 w 的邊當且僅當在 G 中 w 是從 v 可達的。
根據約定,每一個頂點對於本身都是可達的,所以傳遞閉包會含有 V 個自環。上圖只有 22 條有向邊,但它的傳遞閉包含有可能的 169 條有向邊中的 102 條。通常來講,一幅有向圖的傳遞閉包中所含的邊都比原圖中多得多。例如,含有 V 個頂點和 V 條邊的有向環的傳遞閉包是一幅含有 V 的平方條邊的有向徹底圖。由於傳遞閉包通常都是稠密的,咱們一般都將它們表示爲一個布爾值矩陣,其中 v 行 w 列的值爲 true 當且僅當 w 是從 v 可達的。與其計算一幅有向圖的傳遞閉包,不如使用深度優先搜索來實現以下API:
下面的算法使用 DirectedDFS 實現:
namespace Digraphs { public class TransitiveClosure { private DirectedDFS[] all; public TransitiveClosure(Digraph G) { all = new DirectedDFS[G.V()]; for (var v = 0; v < G.V(); v++) all[v] = new DirectedDFS(G,v); } public bool Reachable(int v, int w) { return all[v].Marked(w); } } }
該算法不管對於稀疏圖仍是稠密圖,都是理想解決方案,但對於大型有向圖不適用,由於構造函數所需的空間和 V 的平方成正比,所需的時間和 V(V+ E) 成正比。
總結