判断有向图是否有圈
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的回退边。这篇博客是关于深度优先生成树的介绍:深度优先生成树及其应用。