有向圖環檢測、拓撲排序和有向歐拉圖

內容概要:

  1. DAG圖及有向圖環檢測
  2. 拓撲排序與環檢測
  3. 有向歐拉圖的歐拉回路Hierholzer算法

有向圖環檢測

在某些實際問題抽象出的圖論問題中,要保證研究的圖是一個有向無環圖(Directed Acyclic Graph),如程序模塊的引用,任務調度,學習計劃等等圖模型,所以研究有向無環圖是很有意義的。
有向圖環檢測思路
無向圖進行遍歷過程中如果一個被訪問的頂點再次被訪問到,且這個頂點不是它前一個節點,則說明該無向圖圖中存在環。而有向圖由於邊帶有方向,所以和無向圖環檢測略有不同,在有向圖中,可以加入當前路徑標記,如果遍歷到的頂點在當前路徑上出現過,則說明圖中存在環。
環檢測算法實現

public class DirectedCycleDetection {

    private Graph G;
    private boolean hasCycle;
    private boolean onPath[];// 有向圖環檢測輔助數據
    private boolean[] visited;

    public DirectedCycleDetection(Graph G){

        this.G = G;
        visited = new boolean[G.V()];
        onPath = new boolean[G.V()];

        for(int v = 0; v < G.V(); v ++)
            if(!visited[v])
                if(dfs(v)) {
                    hasCycle = true;
                    break;
                }
    }

    // 從 v 開始檢測是否存在環
    private boolean dfs(int v){
        visited[v] = true;
        onPath[v] = true;

        for(int w: G.adj(v))
            if(!visited[w]) {
                if (dfs(w))
                    return true;
            }
            else if(onPath[w])
                return true;

        onPath[v] = false; // 回溯
        return false;
    }

    public boolean hasCycle(){
        return hasCycle;
    }

    public static void main(String args[]){
        Graph g = new Graph("g2.txt", true);
        DirectedCycleDetection cd = new DirectedCycleDetection(g);
        System.out.println(cd.hasCycle());
    }
}

拓撲排序

在圖論中,一個有向無環圖的頂點組成的序列,當且僅當滿足下列條件時稱爲該圖的一個拓撲排序:

  1. 每個頂點出現且僅出現一次
  2. 若頂點A在序列中排在B的前面,則在圖中不存在從頂點B到頂點A的路徑

拓撲排序算法
從拓撲排序的定義中可以看到,拓撲排序實際上是一種前驅後繼關係,只有DAG圖纔有拓撲排序序列,所以可以這樣來找到圖的拓撲排序序列:
(1)從DAG圖中選擇一個沒有前驅(入度爲0)的頂點並輸出
(2)從圖中刪除該頂點和該頂點的所有出邊
(3)重複(1)和(2)直到DAG圖爲空則選擇頂點的順序就是拓撲排序序列

按照拓撲排序算法的過程同樣可以做有向圖環檢測,如果拓撲排序算法進行到最後不存在無前驅的頂點時圖仍不爲空,說明圖中存在環。

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Queue;

public class TopoSort {
    private Graph G;
    private ArrayList<Integer> res;
    private boolean hasCycle = false;

    public TopoSort(Graph G){
        if(!G.directed)
            throw new IllegalArgumentException("TopoSort only works in directed graph!");
        this.G = G;
        res = new ArrayList<>();
        int[] indegrees = new int[G.V()]; // 對入度的賦值數組操作不影響原圖
        Queue<Integer> q = new LinkedList<>();

        for(int v = 0; v < G.V(); v ++) {
            indegrees[v] = G.indegree(v);
            if(indegrees[v] == 0) q.add(v);
        }

        while(!q.isEmpty()){
            int cur = q.remove();
            res.add(cur);

            for(int w: G.adj(cur)){
                indegrees[w] --;
                if(indegrees[w] == 0) q.add(w);
            }
        }
        if(res.size() != G.V()) {
            hasCycle = true;
            res.clear(); // 有環拓撲排序不存在
        }

    }

    public boolean hasCycle(){
        return hasCycle;
    }
    public ArrayList<Integer> result(){
        return res;
    }

    public static void main(String args[]){
        Graph g = new Graph("g2.txt", true);
        TopoSort ts = new TopoSort(g);
        System.out.println(ts.result());
    }
}

拓撲排序的應用:課程學習順序

LeetCode210

這道題目就是一個典型的拓撲排序問題,構造好題目給定條件下圖的鄰接集合表示,然後按照拓撲排序算法,代碼如下:

// C++
class Solution {
public:
    vector<set<int>>g;
    vector<int>res;
    
    vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites) {
        
        g = vector<set<int>>(numCourses);
        int *indegrees = new int[numCourses];
        memset(indegrees, 0, sizeof(int) * numCourses);
        for(int i=0;i<numCourses;i++)
            indegrees[i] = 0;
        for (int i = 0; i < prerequisites.size(); i++) {      
            g[prerequisites[i][1]].insert(prerequisites[i][0]);
            indegrees[prerequisites[i][0]] ++;
        }
        
        queue<int> q;
        for (int i = 0; i < numCourses; i++)
            if (indegrees[i] == 0)
                q.push(i);
        
        while (!q.empty()) {
            int cur = q.front(); q.pop();
            res.push_back(cur);
            for (auto x : g[cur]) {
                indegrees[x] --;
                if (indegrees[x] == 0) {
                    q.push(x);
                }
            }
        }
        
        if (res.size() != numCourses) return {};
        return res;
    }
};

有向歐拉圖的歐拉回路

入度和出度:頂點的出邊條數稱爲該頂點的出度,頂點的入邊條數稱爲該頂點的入度
初級迴路:即簡單迴路,迴路不經過重複的頂點
有向歐拉圖與半歐拉圖的判定:

  1. G是歐拉圖\LeftrightarrowG中所有頂點的入度等於出度\LeftrightarrowG是若干個邊不重的有向初級迴路的並
  2. G是半歐拉圖\LeftrightarrowG中恰有兩個奇數度頂點,其中一個入度比出度大1,另一個入度比出度小1

求解有向歐拉回路Hierholzer算法
同無向歐拉回路的求解類似,有向歐拉回路的求解也是一步步構造出迴路,最終找到歐拉回路。由有向歐拉圖的充要條件:G是歐拉圖\LeftrightarrowG是若干個邊不重的有向初級迴路的並,我們可以先找到一個初級迴路,而剩下的邊一定還有初級迴路,且這兩個迴路必有公共點,從而可以形成更大的迴路,這樣直到包括所有邊,即可找到歐拉回路。時間複雜度爲O(V+E),非常高效。
設置兩個棧,curPath和loop。算法過程:
(1)選擇任一頂點爲起點,入棧curPath,深度搜索訪問頂點,將經過的邊都刪除,經過的頂點入棧curPath。
(2)如果當前頂點沒有相鄰邊,則將該頂點從curPath出棧到loop。
(3)最後loop棧中的頂點的出棧順序,就是從起點出發的歐拉回路。

(注意和無向圖的區別,無向圖的歐拉回路逆序仍是歐拉回路,但有向圖不是。)

import java.util.ArrayList;
import java.util.Collections;
import java.util.Stack;

public class DirectedEulerLoop {
    private Graph G;
    public DirectedEulerLoop(Graph G){
        this.G = G;
    }
    public boolean hasEulerLoop(){

        for(int v = 0; v < G.V(); v ++)
            if(G.indegree(v) != G.outdegree(v))
                return false;
        return true;
    }
    public ArrayList<Integer> result(){
        // 返回歐拉回路結果
        ArrayList<Integer> res = new ArrayList<>();// 充當Loop棧
        if(!hasEulerLoop()) return res;
        Graph g = (Graph) G.clone();// 用 G 的副本 g 尋找歐拉回路
        // 刪除 g 的邊不會影響 G
        Stack<Integer> stack = new Stack<>(); // curPath 棧
        int curv = 0;
        stack.push(curv);
        while (!stack.isEmpty()){
            if(g.outdegree(curv) != 0){
                // 出度不爲0說明當前頂點連的還有邊,也就是還有路可走
                stack.push(curv);
                int w = g.adj(curv).iterator().next(); // 可迭代列表的第一個元素,即取g的任意鄰點
                g.removeEdge(curv, w);
                curv = w;
            }else {
                // curv 到不了其它頂點,則已經找到一個環
                res.add(curv);
                curv = stack.pop();
            }
        }
        Collections.reverse(res);
        return res;
    }
    public static void main(String args[]){
        Graph g = new Graph("g3.txt", true);
        DirectedEulerLoop el = new DirectedEulerLoop(g);
        System.out.println(el.result());
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章