內容概要:
- DAG圖及有向圖環檢測
- 拓撲排序與環檢測
- 有向歐拉圖的歐拉回路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());
}
}
拓撲排序
在圖論中,一個有向無環圖的頂點組成的序列,當且僅當滿足下列條件時稱爲該圖的一個拓撲排序:
- 每個頂點出現且僅出現一次
- 若頂點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;
}
};
有向歐拉圖的歐拉回路
入度和出度:頂點的出邊條數稱爲該頂點的出度,頂點的入邊條數稱爲該頂點的入度
初級迴路:即簡單迴路,迴路不經過重複的頂點
有向歐拉圖與半歐拉圖的判定:
- G是歐拉圖G中所有頂點的入度等於出度G是若干個邊不重的有向初級迴路的並
- G是半歐拉圖G中恰有兩個奇數度頂點,其中一個入度比出度大1,另一個入度比出度小1
求解有向歐拉回路Hierholzer算法
同無向歐拉回路的求解類似,有向歐拉回路的求解也是一步步構造出迴路,最終找到歐拉回路。由有向歐拉圖的充要條件:G是歐拉圖G是若干個邊不重的有向初級迴路的並,我們可以先找到一個初級迴路,而剩下的邊一定還有初級迴路,且這兩個迴路必有公共點,從而可以形成更大的迴路,這樣直到包括所有邊,即可找到歐拉回路。時間複雜度爲,非常高效。
設置兩個棧,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());
}
}