1. 拓撲排序
拓撲排序是對有向無圈圖的頂點的一種排序:如果存在一條vi到vj的路徑,則vj排在vi后面(因為只要滿足這個特性就是拓撲序列,所以它不一定是唯一的)。比如在眾多的大學課程中,有些課有先修課,我們可以將其抽象為拓撲排序,有向邊(v, w)表明課程v必須安排在w之前,否則課程w就無法進行。我們可以想象所有的課程以及課與課之間的關系可以用一個圖來表示,而拓撲排序就可以知道課程安排的順序。然而,如果圖存在圈,就沒有拓撲序列。比如如果要上課程A必須上課程B,要上課程B必須上課程C,而要上課程C必須上課程A,你將無法選擇哪門課上前面。雖然有圈圖沒有拓撲序列,但是我們可以利用拓撲排序的算法來判斷一個有向圖是否有圈。
算法描述如下:
1. 將所有入度為0的頂點放入隊列;
2. 每次從隊列中彈出一個頂點v(即訪問到該頂點,counter++)直到隊列為空;
3. 遍歷所有與v相連的頂點,將相鄰頂點的入度減一(刪邊);
4. 若某個相鄰頂點入度為0,將其放入隊列中,返回第2步;
5. 若counter == N也就是所有頂點均訪問到,說明排序完成。否則,說明總
有頂點入度不為0,沒有放入隊列中,即該有向圖有圈。
代碼如下:
#include <cstdio> #include <queue> using namespace std; const int MAX_N = 110; vector<int> graph[MAX_N]; //鄰接表存儲圖 int indegree[MAX_N]; //入度 int n; //頂點數 int m; //邊數 bool TopSort() { queue<int> que; int counter = 0; //記錄訪問到的頂點數 for (int i = 1; i <= n; i++) { if (indegree[i] == 0) //將入度為0的頂點全部放入隊列 que.push(i); } while (!que.empty()) { int v = que.front(); que.pop(); counter++; for (int i = 0; i < graph[v].size(); i++) { if (--indegree[graph[v][i]] == 0) que.push(graph[v][i]); } } if (counter != n) //如果有圈,排序失敗 return false; else return true; } int main() { while(scanf("%d%d",&n, &m) != EOF) { for(int i = 1; i <= n; i++) //將圖置空 graph[i].clear(); for(int i=0;i<m;i++) { int u, v; scanf("%d%d", &u, &v); graph[u].push_back(v); } if(TopSort()) printf("Graph does not have a cycle.\n"); else printf("Graph has a cycle.\n"); } return 0; }
2. DFS
關於DFS的介紹請戳我,通過稍微修改DFS,利用遞歸的特點,也可以判斷有向圖是否有圈。修改想法是把原來的visited[]只有true,false兩種狀態改成如下:
vis[u] = 0代表還沒訪問;
vis[u] = -1代表正在訪問中;
vis[u] = 1代表訪問完全;
如果某個點在訪問過程中訪問了兩次,說明出現了環。
用如下樣例模擬出遞歸過程幫助理解。
圖解如下(好吧,畫的有點丑,將就看吧(●'◡'●)):
樣例一(有環):
3 3
1 2
2 3
3 1

樣例二(無環):
3 3
1 2
2 3
1 3

相信通過上面兩幅圖應該可以大致理解了,現在上代碼。
代碼如下:
#include<cstdio> #include<cstring> #include<queue> #include<vector> using namespace std; const int MAX_N = 110; vector<int> graph[MAX_N]; int vis[MAX_N], n, m; //n, m分別是頂點數和邊數 bool DFS(int u) { vis[u] = -1; //-1用來表示頂點u正在訪問 for(int i = 0 ; i < graph[u].size() ; i ++) { if(vis[graph[u][i]] == -1)//表示這個點試探了兩次,肯定出現了環 return false; else if(vis[graph[u][i]] == 0) { if(!DFS(graph[u][i])) return false; } } vis[u] = 1; return true; } bool NoCycle() { memset(vis, 0, sizeof(vis)); //初始化 for(int i = 1 ; i <= n ; i ++) //圖可能不連通 { if(!vis[i]) { if(!DFS(i)) return false; } } return true; } int main() { while(scanf("%d%d",&n, &m) != EOF) { for(int i=1;i<=n;i++) graph[i].clear(); for(int i=0;i<m;i++) { int u,v; scanf("%d%d",&u,&v); graph[u].push_back(v); } if(NoCycle()) printf("Graph does not have a cycle.\n"); else printf("Graph has a cycle.\n"); } return 0; }
上述利用DFS判斷有向圖是否有圈實際上是利用了深度優先生成樹的性質:有向圖無圈當且僅當其深度優先生成樹沒有回退邊, 而上述算法中的vis[graph[u][i]] == -1就是代表有一條u到i的回退邊。這篇博客是關於深度優先生成樹的介紹:深度優先生成樹及其應用。
