有向圖
有向圖同無向圖的區別為每條邊帶有方向,表明從一個頂點至另一個頂點可達。有向圖的演算法多依賴深度搜尋演算法。
本文主要介紹有向圖的基本演算法,涉及圖的表示、可達性、檢測環、圖的遍歷、拓撲排序以及強連通檢測等演算法。
1 定義有向圖
採用鄰接表結構儲存邊資訊,同時提供reverse介面生成反向圖,倒置每個邊的方向,該介面在後續其他演算法中會用到。
/**
* 採用鄰接表表示的有向圖
*/
public class DiGraph {
private final int V;
private int E;
private ArrayList<Integer>[] adj;
public DiGraph(int V)
{
this.V = V;
E = 0;
adj = new ArrayList[V];
for (int i = 0; i < V; i++) {
adj[i] = new ArrayList<>();
}
}
public DiGraph(Scanner scanner)
{
this(scanner.nextInt());
int E = scanner.nextInt();
for (int i = 0; i < E; i++) {
int v = scanner.nextInt();
int w = scanner.nextInt();
addEdge(v, w);
}
}
public void addEdge(int v, int w)
{
// 新增一條v指向w的邊
adj[v].add(w);
E++;
}
/**
* 返回有向圖的反向圖, 將每條邊的方向反轉
*/
public DiGraph reverse()
{
DiGraph diGraph = new DiGraph(V);
for (int v = 0; v < V; v++) {
for (int w : adj[v]) {
diGraph.addEdge(w, v);
}
}
return diGraph;
}
public void show() {
System.out.println("V: " + V);
System.out.println("E: " + E);
for (int i = 0; i < V; i++) {
System.out.print(i + ": ");
for (Integer integer : adj[i]) {
System.out.print(integer + " ");
}
System.out.println();
}
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
// 輸入用例參加附錄1
DiGraph diGraph = new DiGraph(scanner);
// 輸入結果見附錄2
diGraph.show();
}
}
2 有向圖的可達性
有向圖的可達性是指給定一個或一組頂點,判斷是否可以到達圖中其他頂點。垃圾清除常見演算法“標記-清除”演算法中,採用有向圖的可達性演算法
標記所有可以被訪問的物件,然後在回收階段,僅僅回收那些未被標記的物件。
/**
* 基於深度優先的有向圖可達性演算法
* 求出給定頂點或一組頂點,有向圖中能到達的點
*/
public class DirectedDFS {
private boolean[] marked; // 標記每個頂點是否可到達
public DirectedDFS(DiGraph G, int s)
{
marked = new boolean[G.V()];
dfs(G, s);
}
public DirectedDFS(DiGraph G, Iterable<Integer> sources)
{
marked = new boolean[G.V()];
for (int v : sources) {
if(!marked[v]){
dfs(G, v);
}
}
}
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 static void main(String[] args) {
// 輸入用例參加附錄1
DiGraph diGraph = new DiGraph(new Scanner(System.in));
// 輸出結果參加附錄3
// 測試頂點2到達的點
System.out.println("頂點2到達的點");
DirectedDFS reachable = new DirectedDFS(diGraph, 2);
for (int i = 0; i < diGraph.V(); i++)
if(reachable.marked(i)) System.out.print(i + " ");
System.out.println();
// 測試一組點:1,2,6能夠到達的點
System.out.println("1,2,6能夠到達的點");
DirectedDFS reachable2 = new DirectedDFS(diGraph, Arrays.asList(1, 2, 6));
for (int i = 0; i < diGraph.V(); i++)
if(reachable2.marked(i)) System.out.print(i + " ");
System.out.println();
}
}
3 單點有向路徑和單點最短有向路徑
分別採用深度優先搜尋和廣度優先搜尋實現
有向圖的路徑
/**
* 單點有向路徑,給定頂點v,確定對於圖中任一點w;
* 是否存在v到w的路徑,並輸出路徑;
* 注意,深度優先搜尋的路徑無法保證是最短路徑
*/
public class DigraghDepthFirstPaths {
// 標記點是否可達
private boolean[] marked;
// 記錄到達點的那條邊
private int[] edge;
private final int s;
public DigraghDepthFirstPaths(DiGraph G, int s)
{
this.s = s;
marked = new boolean[G.V()];
edge = new int[G.V()];
edge[s] = s;
dfs(G, s);
}
private void dfs(DiGraph G, int v)
{
marked[v] = true;
for (int w : G.adj(v)) {
if(!marked[w]){
edge[w] = v;
dfs(G, w);
}
}
}
public boolean hasPathTo(int v){ return marked[v]; }
public Stack<Integer> pathTo(int v)
{
Stack<Integer> paths = new Stack<>();
for (int x=v; x!=s; x=edge[x]){
paths.add(x);
}
paths.add(s);
return paths;
}
public static void main(String[] args) {
// 輸入用例參加附錄1
DiGraph diGraph = new DiGraph(new Scanner(System.in));
// 輸出結果參加附錄4
// 構建頂點0到其他頂點的有向路徑
DigraghDepthFirstPaths depthFirstPaths = new DigraghDepthFirstPaths(diGraph, 0);
System.out.print("頂點0可達的點: ");
for (int i = 0; i < diGraph.V(); i++) {
if (depthFirstPaths.hasPathTo(i)) System.out.print(i + " ");
}
System.out.println();
// 是否存在有向路徑
if(depthFirstPaths.hasPathTo(12))
System.out.println("0至12存在有向路徑");
else
System.out.println("0至12不存在有向路徑");
// 頂點0到頂點3的一條有向路徑
System.out.print("0至3的一條有向路徑: ");
Stack<Integer> pathTo = depthFirstPaths.pathTo(3);
while (!pathTo.isEmpty()){
if (pathTo.size() == 1)
System.out.print(pathTo.pop());
else
System.out.print(pathTo.pop() + " -> ");
}
System.out.println();
}
}
有向圖的最短路徑,基於廣度優先演算法
/**
* 基於廣度優先搜尋的單向路徑演算法;
* 在此方法下,求得的路徑為最短路徑(忽略邊權重)
*/
public class DigraphBreadthFirstPaths {
private boolean[] marked;
// 採用佇列保持帶訪問的頂點
private ArrayDeque<Integer> enqueue;
private int[] edge;
private final int s;
public DigraphBreadthFirstPaths(DiGraph G, int s)
{
this.s = s;
marked = new boolean[G.V()];
edge = new int[G.V()];
enqueue = new ArrayDeque<>();
enqueue.add(s);
bfs(G);
}
private void bfs(DiGraph G)
{
while (!enqueue.isEmpty())
{
int v = enqueue.poll();
for (int w : G.adj(v)) {
if(!marked[w]){
edge[w] = v;
marked[w] = true;
enqueue.add(w);
}
}
}
}
public boolean hasPathTo(int v){ return marked[v]; }
public Stack<Integer> pathTo(int v)
{
Stack<Integer> paths = new Stack<>();
for (int x=v; x!=s; x=edge[x]){
paths.add(x);
}
paths.add(s);
return paths;
}
public static void main(String[] args) {
// 輸入用例參加附錄1
DiGraph diGraph = new DiGraph(new Scanner(System.in));
// 輸出結果參加附錄5
// 構建頂點0到其他頂點的有向路徑
DigraphBreadthFirstPaths breadthFirstPaths = new DigraphBreadthFirstPaths(diGraph, 0);
System.out.print("頂點0可達的點: ");
for (int i = 0; i < diGraph.V(); i++) {
if (breadthFirstPaths.hasPathTo(i)) System.out.print(i + " ");
}
System.out.println();
// 是否存在有向路徑
if(breadthFirstPaths.hasPathTo(12))
System.out.println("0至12存在有向路徑");
else
System.out.println("0至12不存在有向路徑");
// 頂點0到頂點3的最短路徑
System.out.print("0至3的一條有向路徑: ");
Stack<Integer> pathTo = breadthFirstPaths.pathTo(3);
while (!pathTo.isEmpty()){
if (pathTo.size() == 1)
System.out.print(pathTo.pop());
else
System.out.print(pathTo.pop() + " -> ");
}
System.out.println();
}
}
4 檢測有向圖的環
檢測有向圖是否包含環,檢測圖沒有環是拓撲排序的前提條件。
多數情況下,需要知道有向圖是否包含環,並且輸出夠成環的邊。
/**
* 基於深度優先搜尋檢測圖中是否包含環
*/
public class DirectedCycle {
private boolean[] onStack;
private Stack<Integer> cycle;
private int[] edge;
private boolean[] marked;
public DirectedCycle(DiGraph G)
{
onStack = new boolean[G.V()];
edge = new int[G.V()];
marked = new boolean[G.V()];
for (int i = 0; i < G.V(); i++) {
if(!marked[i])
dfs(G, i);
}
}
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]){
edge[w] = v; dfs(G, w); }
// onStack[w]為true表明,當前v節點是一條經過w的抵達,表明w -> v有路徑
// 由於v -> w有邊,因此必為環
else if(onStack[w]){
cycle = new Stack<>();
for (int x = v; x != w; x=edge[x])
cycle.push(x);
cycle.push(w);
cycle.push(v);
}
}
onStack[v] = false;
}
public boolean hasCycle(){ return cycle != null; }
public Iterable<Integer> cycle() { return cycle; }
public static void main(String[] args) {
// 輸入用例參加附錄1
DiGraph diGraph = new DiGraph(new Scanner(System.in));
// 輸出結果參加附錄6
DirectedCycle directedCycle = new DirectedCycle(diGraph);
System.out.println("有向圖是否包含環: " + (directedCycle.hasCycle() ? "是" : "否"));
if (directedCycle.hasCycle()){
System.out.print("其中一條環為:");
for (int i : directedCycle.cycle()) {
System.out.print(i + " ");
}
}
System.out.println();
}
}
5 頂點的深度優先次序
頂點的深度優先次序分為前序、後序和逆後續,區別是記錄點的時機發生在遞迴呼叫的前還是後。該演算法產生的pre、post和reversePost
順序在圖的高階演算法中十分有用。
public class DepthFirstOrder {
private boolean[] marked;
private ArrayDeque<Integer> pre; // 儲存前序遍歷的結果
private ArrayDeque<Integer> post; // 儲存後序的遍歷結果
private ArrayDeque<Integer> reversePost; //儲存逆後序的遍歷結果
public DepthFirstOrder(DiGraph G)
{
marked = new boolean[G.V()];
pre = new ArrayDeque<>();
post = new ArrayDeque<>();
reversePost = new ArrayDeque<>();
for (int v=0; v<G.V(); v++)
if (!marked[v]) dfs(G, v);
}
private void dfs(DiGraph G, int v)
{
marked[v] = true;
pre.add(v);
for (int w : G.adj(v))
if(!marked[w])
dfs(G, w);
post.add(v);
// 按post的倒序儲存
reversePost.addFirst(v);
}
public Iterable<Integer> pre(){ return pre; }
public Iterable<Integer> post(){ return post; }
public Iterable<Integer> reversePost(){ return reversePost; }
public static void main(String[] args) {
// 構造無環圖的輸入參見附錄7
DiGraph diGraph = new DiGraph(new Scanner(System.in));
DepthFirstOrder depthFirstOrder = new DepthFirstOrder(diGraph);
// 輸出結果參加附錄8
// 注意:對於同一幅圖,構造圖的輸入順序不一致
// 會導致輸出不相同
System.out.print("前序節點順序: ");
for (int v : depthFirstOrder.pre())
System.out.print(v + " ");
System.out.println();
System.out.print("後續節點順序:");
for (int v : depthFirstOrder.post())
System.out.print(v + " ");
System.out.println();
System.out.print("逆後序節點順序:");
for (int v : depthFirstOrder.reversePost())
System.out.print(v + " ");
}
}
6 拓撲排序
給定一幅有向圖,給出一組頂點排序,在有向圖中,所有的邊均是前面的點指向後面的點。
拓撲排序依賴圖的環檢測和逆後序遍歷演算法。
/**
* 計算有向無環圖中的所有頂點的拓撲排序,
* 通常用於解決優先順序限制下的排程問題
*/
public class Topological {
private Iterable<Integer> order;
public Topological(DiGraph G)
{
DirectedCycle directedCycle = new DirectedCycle(G);
if(!directedCycle.hasCycle())
order = new DepthFirstOrder(G).reversePost();
}
public boolean isDAG(){ return order == null; }
public Iterable<Integer> order(){ return order; }
public static void main(String[] args) {
// 輸入用例參考附錄7
DiGraph diGraph = new DiGraph(new Scanner(System.in));
Topological topological = new Topological(diGraph);
// 輸出結果參見附錄9
if (topological.isDAG())
System.out.println("有向圖帶有環,無法進行拓撲排序");
else{
System.out.print("拓撲排序結果:");
for (int v : topological.order()) {
System.out.print(v + " ");
}
}
}
}
7 強聯通檢測
如果存在從v至w的路徑,同時還存在從w至v的路徑,則稱v和w之間是強連通;如果一幅有向圖中任意兩點間都
是強連通,則這幅有向圖也是強連通的。檢測強連通演算法依賴圖的反轉和逆後序遍歷演算法。演算法比較簡潔,但是
理解起來比較難,需要仔細分析理解。
/**
* 有向圖的強連通性,該演算法依賴逆後序排序、圖的反轉、無向圖的聯通性演算法
*/
public class SCC {
private int[] id;
private int count;
private boolean[] marked;
public SCC(DiGraph G)
{
id = new int[G.V()];
marked = new boolean[G.V()];
DepthFirstOrder depthFirstOrder = new DepthFirstOrder(G.reverse());
for (int v : depthFirstOrder.reversePost())
if(!marked[v]) {
dfs(G, v);
count++;
}
}
private void dfs(DiGraph G, int v)
{
id[v] = count;
marked[v] = true;
for (int w : G.adj(v))
if(!marked[w])
dfs(G, w);
}
// 兩點是否是強連通
public boolean stronglyConnected(int v, int w){ return id[v] == id[w]; }
// 強聯通分量數
public int count(){ return count; }
// 節點所在的聯通分量識別符號
public int id(int v){ return id[v]; }
public static void main(String[] args) {
// 帶環的圖,輸入用例參見附錄1
DiGraph diGraph = new DiGraph(new Scanner(System.in));
// 輸出結果參見附錄10
SCC scc = new SCC(diGraph);
System.out.println("有向圖中強連通分量數:" + scc.count());
System.out.println("節點6與12是否是強連通:" + (scc.stronglyConnected(6, 12) ? "是" : "否"));
System.out.println("節點9與12是否是強連通:" + (scc.stronglyConnected(9, 12) ? "是" : "否"));
System.out.println("輸出聯通分量");
for (int i = 0; i < scc.count(); i++) {
for (int v = 0; v < diGraph.V(); v++) {
if(scc.id[v] == i)
System.out.print(v + " ");
}
System.out.println();
}
}
}
附錄1,有向圖構造資料
13
22
4 2
2 3
3 2
6 0
0 1
2 0
11 12
12 9
9 10
9 11
8 9
10 12
11 4
4 3
3 5
7 8
8 7
5 4
0 5
6 4
6 9
7 6
附錄2,有向圖輸出
V: 13
E: 22
0: 1 5
1:
2: 3 0
3: 2 5
4: 2 3
5: 4
6: 0 4 9
7: 8 6
8: 9 7
9: 10 11
10: 12
11: 12 4
12: 9
附錄3:有向圖的可達性測試
頂點2到達的點
0 1 2 3 4 5
1,2,6能夠到達的點
0 1 2 3 4 5 6 9 10 11 12
附錄4:基於深度優先搜尋的單向路徑測試結果
頂點0可達的點: 0 1 2 3 4 5
0至12不存在有向路徑
0至3的一條有向路徑: 0 -> 5 -> 4 -> 2 -> 3
附錄5:基於廣度優先搜尋的最短路徑測試結果
頂點0可達的點: 0 1 2 3 4 5
0至12不存在有向路徑
0至3的一條有向路徑: 0 -> 5 -> 4 -> 3
附錄6:檢測環演算法的測試輸出
有向圖是否包含環: 是
其中一條環為:3 2 4 5 3
附錄7:構造無環圖的輸入用例
13
15
0 1
0 5
0 6
2 0
2 3
3 5
5 4
6 4
6 9
7 6
8 7
9 10
9 11
9 12
11 12
附錄8:深度優先遍歷圖的輸出結果
前序節點順序: 0 1 5 4 6 9 10 11 12 2 3 7 8
後續節點順序:1 4 5 10 12 11 9 6 0 3 2 7 8
逆後序節點順序:8 7 2 3 0 6 9 11 12 10 5 4 1
附錄9:拓撲排序測試輸出結果
拓撲排序結果:8 7 2 3 0 6 9 11 12 10 5 4 1
附錄10:帶環有向圖的強連通性測試輸出結果
有向圖中強連通分量數:5
節點6與12是否是強連通:否
節點9與12是否是強連通:是
輸出聯通分量
1
0 2 3 4 5
9 10 11 12
6
7 8