1. 程式人生 > 其它 >《演算法導論》——深度優先搜尋與拓撲排序

《演算法導論》——深度優先搜尋與拓撲排序

深度遍歷演算法描述

演算法描述參考自《演算法導論》深度優先搜尋演算法:

/*
註解:
1、G.v表示途中節點的集合,其中G是一個有向圖
2、G:Adj[u] 表示再有向圖中以u為起始節點的鄰接節點集合
3、color 白色表示節點未被發現;灰色表示節點已經被發現但沒有深搜完畢;黑色節點表示節點深搜完畢
4、u.d 表示節點訪問的開始時間。u.f表示節點訪問的結束時間。u.parent表示u的父節點,NIL表示父節點為空。
*/
DFS(G)
    for each vertex u belong to G.V
        u.color=WHITE
        u.parent=NIL
    time
=0 for each vertex u belong to G.V if u.color=WHITE DFS-VISIT(G,u) DFS-VISIT(G,u) time=time+1 u.d=time u.color=GRAY for each v belong to G:Adj[u] if v.color==WHITE v.parent=u DFS-VISIT(G,v) u.coloe=BALCK time=time+1 u.f
=time

什麼是拓撲排序

對一個有向無環圖(Directed Acyclic Graph簡稱DAG)G進行拓撲排序,是將G中所有頂點排成一個線性序列,使得圖中任意一對頂點u和v,若邊∈E(G),則u線上性序列中出現在v之前。通常,這樣的線性序列稱為滿足拓撲次序(Topological Order)的序列,簡稱拓撲序列。簡單的說,由某個集合上的一個偏序得到該集合上的一個全序,這個操作稱之為拓撲排序。——百度百科

注意:若有向圖中存在迴路,則該有向圖無法進行拓撲排序

常用的拓撲排序方法如下:

方法一:入度統計
(1)從有向圖中選擇一個沒有前驅(即入度為0)的頂點並且輸出它。

(2)從圖中刪去該頂點,並且刪去從該頂點發出的所有邊。

(3)重複上述步驟(1)和(2),直到當前有向圖中不存在沒有前驅結點的頂點為止,或者當前有向圖中的所有結點均已輸出為止。

(4)如果當前有向圖中不存在沒有前驅結點的頂點,並且當前有向圖中的所有結點尚未完全輸出,則可以判斷當前有向圖中有環

方法二:深度優先搜尋

思路是記錄各個節點深度遍歷時完成訪問的結束時間,然後根據結束時間的先後順序組成一個列表,則該列表就是一個拓撲序列

拓撲排序DFS演算法描述

演算法描述參考自《演算法導論》拓撲排序:

TOPOLOGICAL-SORT(G)
    call DFS(G) to compute finishing times v.f for each vertex v
    as each vertex is finished,insert it onto the front of a linked list
    return the linked list of vertices

需要注意的是,基於深度優先搜尋來實現拓撲排序,所用到的圖必須是有向無環

DFS程式碼實現

package myDFS;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Scanner;

public class DFS {
    private int nodeNum;//圖的節點個數
    private int[][] graph;//鄰接矩陣儲存圖資訊
    private List<Edge> edges = new ArrayList<>();//儲存邊資訊
    private List<Integer> res = new ArrayList<>();//存放深度遍歷的路徑
    private List<Integer> topoSort = new ArrayList<>();//存放拓撲排序路徑

    public static void main(String[] args) {
        DFS dfs = new DFS();
        dfs.inputInfo();    //輸入圖的資訊
        dfs.creatGraph();   //建立有向圖
        dfs.outputGraph();  //列印有向圖
        dfs.dfs();          //進行深度遍歷
        dfs.outputRes();    //輸出結果
    }

    /**
     * 使用者輸入,並建立邊集合
     */
    public void inputInfo() {
        Scanner scanner = new Scanner(System.in);
        System.out.print("請輸入節點個數:");
        nodeNum = scanner.nextInt();
        int size = nodeNum;
        System.out.println("請依次輸入邊,格式為:起始節點 終止節點(-1 -1作為結束符)");
        while (true) {
            int node1 = scanner.nextInt();
            int node2 = scanner.nextInt();
            if (node1 < 0 || node2 < 0)
                break;
            edges.add(new Edge(node1, node2));
        }
    }

    /**
     * 建立有圖
     */
    public void creatGraph() {
        graph = new int[nodeNum + 1][nodeNum + 1];//因為節點從1開始編號,所以加一
        for (int i = 0; i < edges.size(); i++) {
            graph[edges.get(i).node1][edges.get(i).node2] = 1;
        }
    }

    /**
     * 深度遍歷遞迴呼叫
     */
    static int time = 0;

    public void dfs() {
        // visited訪問標誌符陣列,有三種狀態:
        // visited=0表示節點未被發現
        // visited=1表示節點已經被發現,但沒有完成深搜
        // visited=2表示節點已經深搜完畢
        int visited[] = new int[nodeNum + 1];
        int startTime[] = new int[nodeNum + 1];//用於記錄各節點被發現的時間
        int endTime[] = new int[nodeNum + 1];//用於記錄各節點完成深搜的時間
        time = 0;//全域性時鐘

        //選擇開始節點進行深搜
        for (int i = 1; i <= nodeNum; i++) {
            if (visited[i] == 0) {
                dfs(graph, i, visited, startTime, endTime);
            }
        }
        System.out.println("startT:" + Arrays.toString(startTime));
        System.out.println("endT:" + Arrays.toString(endTime));
    }

    /**
     * 遞迴實現深度遍歷
     *
     * @param graph   有向圖
     * @param node    當前節點
     * @param visited 訪問識別符號陣列
     * @param startT  開始時間陣列
     * @param endT    結束時間陣列
     */
    public void dfs(int graph[][], int node, int visited[], int startT[], int endT[]) {
        res.add(node);//記錄訪問路徑
        time++;       //節點被發現,更新時鐘
        visited[node] = 1;
        startT[node] = time;
        while (findNext(graph, node, visited) != -1) {
            int nestNode = findNext(graph, node, visited);
            if (visited[nestNode] == 0) {
                dfs(graph, nestNode, visited, startT, endT);
            }
        }
        time++;//節點深搜結束,更新時鐘
        endT[node] = time;
        visited[node] = 2;
        topoSort.add(node);
    }

    /**
     * 獲取當前節點的下一個節點
     *
     * @param graph   有向圖
     * @param node    當前節點
     * @param visited 訪問標誌符陣列
     * @return -1表示沒有下一個節點
     */
    private int findNext(int[][] graph, int node, int visited[]) {
        for (int i = 0; i < edges.size(); i++) {
            Edge edge = edges.get(i);
            if (edge.node1 == node && visited[edge.node2] == 0) {
                return edge.node2;
            }
        }
        return -1;
    }

    /**
     * 列印有向圖
     */
    public void outputGraph() {
        for (int i = 1; i <= nodeNum; i++) {
            for (int j = 1; j <= nodeNum; j++) {
                System.out.print(graph[i][j] + " ");
            }
            System.out.println();
        }
    }

    /**
     * 輸出深度遍歷的路徑以及拓撲排序路徑
     */
    public void outputRes() {
        System.out.println("訪問路徑" + res);
        System.out.println("拓撲路徑" + topoSort);
    }

}

class Edge {
    int node1;
    int node2;
    int weight;

    public Edge(int node1, int node2) {
        this.node1 = node1;
        this.node2 = node2;
    }
}

測試用例:

6

1 2
1 3
1 4
2 3
2 5
3 4
3 5
4 6
5 6
-1 -1

輸出:

0 1 1 1 0 0
0 0 1 0 1 0
0 0 0 1 1 0
0 0 0 0 0 1
0 0 0 0 0 1
0 0 0 0 0 0
startT:[0, 1, 2, 3, 4, 8, 5]
endT:[0, 12, 11, 10, 7, 9, 6]
訪問路徑[1, 2, 3, 4, 6, 5]
拓撲路徑[6, 4, 5, 3, 2, 1]

參考資料

  1. 《演算法導論》深度優先搜尋
  2. 什麼是拓撲排序
  3. 拓撲排序及其Java實現