有向圖
相較於無向圖,有向圖的邊是帶有方向性的。v→w,那麼v的鄰接點鏈表中會有w,但是w的鄰接點鏈表不存在v。因此就鄰接表來說,體現有向性是通過鏈表中的節點有無來實現的。
public class Digraph {
private final int V;
private int E;
// 邊的有向性體現在結點添加時,不像之前無向圖,兩端結點可以互達。
// 有向圖的節點是不一定可互達的
private Bag<Integer>[] adj;
public Digraph(int V) {
this.V = V;
this.E = 0;
adj = (Bag<Integer>[]) new Bag[V];
for (int i = 0; i < V; i++) {
adj[V] = new Bag<>();
}
}
public int V() {
return V;
}
public int E() {
return E;
}
public void addEdge(int v, int w) {
adj[v].add(w);
E++;
}
public Iterable<Integer> adj(int v) {
return adj[v];
}
public Digraph reverse() {
Digraph R = new Digraph(V);
for (int i = 0; i < V; i++) {
for (int w : adj(i)) {
// 通過相反的方式添加邊
R.addEdge(w, i);
}
}
return R;
}
}
有向圖的可達性
public class DirectedDFS {
private boolean[] marked;
public DirectedDFS(Digraph G, int s) {
marked = new boolean[G.V()];
dfs(G, s);
}
private void dfs(Digraph G, int v) {
marked[v] = true;
for (int w : G.adj(v)) {
if (!marked[w])
dfs(G, w);
}
}
public boolean marked(int v){
return marked[v];
}
}
拓撲排序
定義:給定一幅有向圖,將所有的頂點排序,使得所有的有向邊均從排在前面的元素指向排在後面的元素
優先級調度問題中,環的存在時不允許的,這會打破優先級熟悉怒,因此環的檢測十分重要。
環檢測算法及其對應的數據結構:
public class DirectedCycle {
private boolean[] marked;
private int[] edgeTo;
private Stack<Integer> cycle; // 如果有環,創建這個環
private boolean[] onStack; // 保存遞歸調用時,棧上的所有頂點
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;
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<>();
// 構建這個環,依次獲取DFS中頂點的上級頂點
for (int x = v; x != w; x = edgeTo[x]) {
cycle.push(x);
}
cycle.push(w);
cycle.push(v);
}
}
onStack[v] = false;
}
private boolean hasCycle() {
return cycle != null;
}
public Iterable<Integer> cycle() {
return cycle;
}
}
拓撲排序要點
- 在進行DFS時,將參數頂點保存在一個數據結構中,遍歷這個數據結構實際上就能訪問圖中的所有頂點。
- 排序順序(注意數據結構的使用):
- 前序:在遞歸調用之前將頂點加入隊列
- 後序:在遞歸調用之後將頂點加入隊列
- 逆後序:在遞歸調用之後將頂點壓入棧
三種排序方式的數據結構 – 可類比樹的遍歷
public class DepthFirstOrder {
private boolean[] marked;
private Queue<Integer> pre;
private Queue<Integer> post;
private Stack<Integer> reversePost;
public DepthFirstOrder(Digraph G) {
// 用原生JDK實現
pre = new LinkedList<>();
post = new LinkedList<>();
reversePost = new Stack<>();
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.offer(v);
// DFS 過程
marked[v] = true;
for (int w : G.adj(v))
if (!marked[w]) dfs(G, w);
// 逆序
post.offer(v);
// 逆後序
reversePost.push(v);
}
public Queue<Integer> pre() {
return pre;
}
public Queue<Integer> post() {
return post;
}
public Stack<Integer> reversePost() {
return reversePost;
}
}
拓撲算法實現:
public class Topological {
// 頂點的拓撲排序
private Iterable<Integer> order;
public Topological(Digraph G){
// 檢測有無環
DirectedCycle cycleDetector = new DirectedCycle(G);
if(!cycleDetector.hasCycle()){
// 無環則返回節點序列
DepthFirstOrder dfs = new DepthFirstOrder(G);
// 拓撲順序爲逆後序 -- 入度小的在前面
order = dfs.reversePost();
}
}
public Iterable<Integer> order() {
return order;
}
public boolean isDAG(){
return order != null;
}
}
有向圖的強連通性
-
定義:有向圖的兩個頂點v和w,如果是相互可達的,那麼稱他們強連通
-
推論:頂點強連通 當且僅當 它們都在一個普通的有向環中
-
滿足離散數學推論中的:自反性、對稱性、傳遞性
-
定義是基於點而不是邊
強連通分量的計算算法 – Kosaraju算法:
這個算法的理解比較噁心,首先需要理解這個算法的目的:
- 保證在遍歷強連通分量時,有一個其他連通分量的頂點,在這個強連通分量的前面
- 這樣檢驗完一個強連通分量後,可以進入下一個強連通分量
- 強連通分量之間是可達,但不構成迴路的
- 綜上所述,要進行統計之前,首先要構建一個序列,將各個連通分量隔離開(內部順序不定)
接下來是證明通過求反向圖的逆後序,可以保證兩個連通分量之間是隔離開的
可以看到,通過構建反向圖的逆後序排列,成功將各個強連通分量隔離開,並保證可以從一個強連通分量進入下一個強連通分量
public class KosarajuSCC {
private boolean[] marked;
private int[] id;
private int count;
public KosarajuSCC(Digraph G) {
marked = new boolean[G.V()];
id = new int[G.V()];
// 核心部分 -- 其實完成了兩次DFS,在創建order的時候還進行了依次dfs
DepthFirstOrder order = new DepthFirstOrder(G.reverse());
for (int s : order.reversePost()) {
if (!marked[s]) {
dfs(G, s);
count++;
}
}
}
private void dfs(Digraph G, int v) {
marked[v] = true;
id[v] = count;
for (int w : G.adj(v)) {
if (!marked[w]) dfs(G, w);
}
}
public boolean stronglyConnected(int v, int w){
// 如果所屬的連通分量id相同,則證明v和w相互連通
return id[v] == id[w];
}
}