本文主要針對如何判斷有向圖/無向圖中是否存在環的問題進行簡單的論述。
一 無向圖
1.利用DFS進行判斷
利用DFS判斷有向圖是否存在環,是最為常用的一種方法,雖然這種方法很常用,但可參考的代碼的實現比較少,下面對這種方法及其實現進行詳細的闡述。
首先,利用DFS判斷無向圖中是否換的原理是:若在深度優先搜索的過程中遇到回邊(即指向已經訪問過的頂點的邊),則必定存在環。
所以說,是否存在環的關鍵在於是否存在滿足條件的“回邊”,那么如何判斷回邊呢?
(1)首先,對圖中的所有頂點定義三種狀態:頂點未被訪問過、頂點剛開始被訪問、頂點被訪問過並且其所有鄰接點也被訪問過。這三種狀態,在visited數組中分別用0、1、2來表示。那么,存在環的情況可以定義為:在遍歷過程中,發現某個頂點的一條邊指向狀態1的頂點,此時就存在環。狀態2可以理解為其生成樹上的所有的子孫節點都已經訪問完。
(2)此外,我們要定義一個father數組,用以存儲在DFS過程中頂點的父頂點(或者說是生成樹上的父節點)。其主要作用是為了區分鄰接點中環中的頂點和遍歷過程中的父節點 (單純的用visited數組無法區分)。
整個過程的實現代碼如下:
#define MAX_NUM 100 #define INF 0x7fffffff /*DFS判斷無向圖中是否有環*/ class Graph { public: int vertexNum;//頂點個數 int arcNum;//弧的個數 int vertex[MAX_NUM];//頂點表 int arc[MAX_NUM][MAX_NUM];//弧信息表 }; int visited[MAX_NUM];//頂點訪問表 int father[MAX_NUM];//父節點表 void DFS(int v,Graph G) { visited[v] = 1; for(int i = 0 ; i < G.vertexNum; i++) { if(i != v && G.arc[v][i] != INF)//鄰接矩陣中節點v的鄰接點 { if(visited[i] == 1 && father[v] != i)//vi不是父節點,而且還訪問過(而且為狀態1,說明不是回溯過來的頂點),說明存在環(判斷i不是v的父節點) { cout<<"圖存在環"; int temp = v; while(temp != i) { cout<<temp<<"<-";//輸出環 temp = father[temp]; } cout<<temp<<endl; } else if(visited[i] == 0) { father[i] = v;//更新father數組 DFS(i,G); } } } visited[v] = 2;//遍歷完所有的鄰接點才變為狀態2 } void DFSTraverse(Graph G) { memset(visited,0,sizeof(visited)); memset(father,-1,sizeof(father)); for(int i = 0 ; i < G.vertexNum; i++) if(!visited[i]) DFS(i,G); }
由此可見,visited數組相對於一般的情況,增加了個狀態2,主要是為了防止在回溯過程中進行誤判。所以才能僅用father數組和狀態1判斷存在環。
狀態2可以理解為其生成樹上的所有的子孫節點都已經訪問完。
由於使用的是鄰接矩陣來存儲,所以該算法的時間復雜度為O(n^2),空間復雜度為O(n)。
2.其他方法本文不再詳述。
二 有向圖
1.拓撲排序
關於拓撲排序,資料很多,本文不再詳述其原理,只給出其實現代碼,代碼如下:
#include<iostream> #include<unordered_map> #include<queue> #include<cstring> #include<cstdlib> #include<cmath> #include<algorithm> #include<sstream> #include<set> #include<map> #include<stack> using namespace std; #define MAX_NUM 100 #define INF 0x7fffffff /*拓撲排序*/ int indegree[MAX_NUM];//用以表示每個頂點的入度 bool visited[MAX_NUM];//用以表示該頂點是否入棧 class Graph { public: int vertexNum;//頂點個數 int arcNum;//弧的個數 int vertex[MAX_NUM];//頂點表 int arc[MAX_NUM][MAX_NUM]= {{0,1,1},{INF,0,1},{INF,INF,0}}; //弧信息表 }; void Initindegree(Graph G)//初始化入度數組 { memset(indegree,0,sizeof(indegree)); for(int i = 0; i < G.vertexNum; i++) for(int j = 0; j < G.vertexNum; j++) { if(i != j && G.arc[i][j] != INF) indegree[j]++;//注意此處增加的是頂點vj的入度 } memset(visited,0,sizeof(visited)); } bool TuoPu(Graph G) { stack<int> s; int cnt = 0;//用於記錄拓撲序列中節點的個數 for(int i = 0 ; i < G.vertexNum; i++) if(indegree[i] == 0) { s.push(i); visited[i] = true;//修改入棧頂點的入棧標記數組 } while(!s.empty()) { int v = s.top(); cnt++;//頂點出棧得到時候,計數器加1 s.pop(); for(int i = 0; i < G.vertexNum; i++) { if(v != i && G.arc[v][i] != INF && visited[i] == false)//將所有頂點v的未入棧的鄰接點的入度都減去1 { indegree[i]--; if(indegree[i] == 0)//如果減1后入度為0了,此時需要將該鄰接點入棧,且修改入棧標記數組 { visited[i] = true; s.push(i); } } } } return cnt == G.vertexNum ? true : false; } int main() { Graph G; G.vertexNum = 3; Initindegree(G); cout<<TuoPu(G)<<endl; }
2.利用改進的DFS
對於有向圖的話,如果直接應用一般的DFS的話,會出現誤判的情況,一個典型的例子是:A->B,A->C->B,我們用DFS來處理這個圖,我們會得出它有環,但實際上並沒有。然而,本文中所說的無向圖的DFS判斷算法完全可以直接應用到有向圖中來,即上述代碼可以直接應用到有向圖中來。所以說上述的DFS算法(或稱為為改進的DFS算法)既適用於無向圖,也適用於有向圖。其對應的原理適用於這兩種圖,即只要我們在遍歷過程中,只要發現一個頂點不是當前節點的父節點,同時他還被訪問過了(狀態為1),那么就可以認為此處存在環。(通常在DFS中一個頂點的未被訪問的鄰接點,相當於生成樹中的該頂點的子孫節點)