環和有向無環圖
文章目錄
在和有向圖相關的實際應用中,有向環特別的重要。優先順序限制下的排程問題:給定一組需要完成的任務,以及一組關於任務完成先後次序的優先順序限制。在滿足限制條件的前提下應該如何安排並完成所有任務?
對於任意一個這樣的問題,我們都可以畫出一張有向圖,其中頂點對應任務,有向邊對應優先順序順序。 優先順序限制下的排程問題等價於計算有向無環圖中所有頂點的拓撲排序。
定義
給定一幅有向圖,將所有的頂點排序,使得所有的有向邊均從排在前面的元素指向排在後面的元素。
拓撲排序是對有向無環圖的頂點的一種排序, 使得如果存在一條從v到w的路徑,那麼在排序中w就出現在v的後面。 如果圖含有環,那麼拓撲排序是不可能的。試想有3個正整數,a比b大,b比c大,c比a大,我們無法對abc排序。
有向環檢測
如果一個有優先順序限制的問題中存在有向環,那麼這個問題肯定無解。要檢查這種錯誤,需要進行有向環檢測,即給定的有向圖中包含有向環嗎?
只需要找出一個環即可,而不是所有環。
基於深度優先搜尋來解決這個問題並不困難,因為由系統維護的遞迴呼叫的棧表示的正是“當前”正在遍歷的有向路徑。一旦我們找到了一條邊 v->w 且 w 已經存在於棧中,就找到了一個環,因為棧表示的是一條由 w 到 v 的有向路徑,而 v->w 剛好補全了這個環。同時,如果沒找到這樣的邊,就意味著這幅有向圖是無環的。
/*
* 尋找有向環
*/
public class DirectedCycle {
private boolean[] marked;
private int[] edgeTo; // 在找到有向環後,環上的所有頂點可以通過edgeTo[]中的連結得到
private Stack<Integer> cycle; // 有向環中的所有頂點(如果存在)
private boolean[] onStack; // 儲存遞迴呼叫的棧上的所有頂點。當找到一條邊v->w且w在棧中時,就找到了一個有向環
public DirectedCycle(Digraph G) {
onStack = new boolean [G.V()];
edgeTo = new int[G.V()];
marked = new boolean[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; //這個變數是神來之筆。因為有好幾棵樹,但我只要查我所在的這棵樹。所以在遞迴的時候一路把這棵樹標成true,在返回之前再標回false。為下一棵樹可以迴圈再利用做準備。
marked[v] = true;
for (int w : G.adj(v))
if (this.hasCycle())
return;
else if (!marked[w]) {
edgeTo[w] = v;
dfs(G, w);
}
else if (onStack[w]) { // 我遇到了組織,我們形成了一個環!
cycle = new Stack<Integer>();
for (int x = v; x != w; x = edgeTo[x])
cycle.push(x);
cycle.push(w);
cycle.push(v); //v壓了兩次,第一次作為箭頭終點,第二次作為箭頭起點
}
onStack[v] = false;
}
public boolean hasCycle() { return cycle != null; }
public Iterable<Integer> cycle() { return cycle; }
}
基於DFS的頂點排序
先關注 “有向圖中基於深度優先搜尋的頂點排序” 的 DepthFirstOrder 類。 他的基本思想是DFS正好只會訪問每個頂點一次。如果將 dfs() 的引數頂點儲存在一個數據結構中,遍歷這個資料結構實際上就能訪問圖中的所有頂點,遍歷的順序取決於這個資料結構的性質以及是在遞迴呼叫之前還是之後進行儲存。
頂點的3種排列順序: 前序:在遞迴呼叫之前將頂點加入佇列 後序:在遞迴呼叫之後將頂點加入佇列 逆後序:在遞迴呼叫之後將頂點壓入棧
下述演算法允許用例用各種順序遍歷DFS經過的所有頂點
/*
* 有向圖中基於深度優先搜尋的頂點排序
*/
public class DepthFirstOrder {
private boolean[] marked;
private Queue<Integer> pre; // 前序排列
private Queue<Integer> post; // 後序排列
private Stack<Integer> reversePost; // 逆後序排列
public DepthFirstOrder(Digraph G) {
pre = new Queue<Integer>();
post = new Queue<Integer>();
reversePost = new Stack<Integer>();
marked = new boolean[G.V()];
for (int 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;
for (int w : G.adj(v))
if (!marked[w])
dfs(G, w);
post.enqueue(v); //
reversePost.push(v); //
}
public Iterable<Integer> pre() { return pre; }
public Iterable<Integer> post() { return post; }
public Iterable<Integer> reversePost() { return reversePost; }
}
拓撲排序
優先順序限制下的排程問題等價於計算有向無環圖中所有頂點的拓撲排序。
當且僅當一幅有向圖是無環圖時,它才能進行拓撲排序。
下述演算法使用了DepthFirstOrder類 和 DirectedCycle類來返回一幅有向無環圖的拓撲順序。Topological類 的實現使用了DFS來對有向無環圖進行拓撲排序。
/*
* 拓撲排序
*/
public class Topological {
private Iterable<Integer> order; // 頂點的拓撲順序
public Topological(Digraph G) {
DirectedCycle cyclefinder = new DirectedCycle(G);
if (!cyclefinder.hasCycle()) {
DepthFirstOrder dfs = new DepthFirstOrder(G);
order = dfs.reversePost(); //排序方式 逆後序
}
}
public Iterable<Integer> order() { return order; }
public boolean isDAG() { return order == null; }
}
使用深度優先搜尋對有向無環圖進行拓撲排序所需的時間和 成正比。 第一遍DFS保證了不存在有向環,第二遍DFS產生了頂點的逆後序排列。兩次搜尋都訪問了所有的頂點和邊,因此它所需的時間和 成正比。
在實際應用中,拓撲排序和有向環的檢測總會一起出現,因為有向環的檢測是排序的前提。 因此,解決任務排程類應用通常需要以下3步: – 指明任務和優先順序條件 – 不斷檢測並去除有向圖中的所有環,以確保存在可行方案 – 使用拓撲排序解決排程問題