文章目录
- 1. DFS
- (1) 判断环的存在
- (2) 输出环路
- 2. BFS
- (1) 判断环的存在
- (2) 输出环路
- 3. Union-Find
- (1) 原理讲解
- (2) 代码实现
本文针对无向图的判环问题进行论述。
无向图判断环的存在很容易。注意,这里不要求无向图是连通图,如果无向图中的任一连通分量中有环,就说无向图中存在环。
1. DFS
对于无向图来说,如果深度优先遍历过程中遇到回边,即指向已经访问过的顶点的边,就必定存在环。
(1) 判断环的存在
利用DFS判断无向图中是否存在环,是最常用的一种方法。我们对每个未访问过的顶点运行DFS,这会在该顶点所处的连通分量上产生一棵深度优先生成树,图中存在环当且仅当树上存在回边 back edge
,这条回边要么关联自己(自环边),要么关联深度优先生成树上该顶点的某个祖先结点。
为了找到连通祖先结点的回边,我们使用一个 visited
数组,如果有一条指向已经访问过的顶点的回边,那么就有环,我们返回 true
。
无向图DFS判断环的算法步骤如下:
- 使用给定的边数和顶点数创建一个图;
- 使用一个递归函数,访问当前的顶点,标记当前的顶点为访问过;
- 找到当前顶点的所有未访问的邻接点,递归调用函数访问这些顶点,如果递归函数返回
true
,就直接返回true
,不再进行下一步; - 如果某些邻接点不是父结点但却已经被访问和标记过,我们就返回
true
,否则最后返回false
; - 创建一个
wrapper
函数,对无向图中所有未访问过的顶点,都调用该递归函数; - 如果对任一顶点,递归函数返回
true
,就返回true
; - 如果对所有顶点,递归函数都返回
false
,才返回false
。
需要注意的是:这里是简单图,不允许自环的存在;另外,对于一条边关联的两个顶点,DFS时从一个顶点 u
访问其邻接点 v
,u
在深度优先生成树上是 v
的父结点,然后对 v
访问其邻接点时需要特判 u
代码如下:
#include <bits/stdc++.h>
using namespace std;
//判断在一张无向图中是否存在环
//无向图邻接表
class Graph {
private:
int numVertexes; //顶点编号数0-numVertexes
list<int> *adj; //指向一个邻接链表数组的指针
bool isCyclicUtilDFS(int v, bool visited[], int parent);
public:
Graph(int numVertexes); //传入顶点数
~Graph(); //析构函数
void addEdge(int v, int w); //加入边到图中
bool isCyclicDFS(); //如果有环返回true
};
Graph::Graph(int numVertexes) {
this->numVertexes = numVertexes;
adj = new list<int>[numVertexes];
}
Graph::~Graph() {
delete [] adj;
}
void Graph::addEdge(int v, int w) {
if (v == w) return; //不允许自环
adj[v].push_back(w);
adj[w].push_back(v);
}
//递归算法,使用visited[]和parent判断
//顶点v可达的子图(连通分量)中是否存在环
bool Graph::isCyclicUtilDFS(int v, bool visited[], int parent) {
visited[v] = true; //标记当前结点访问过
//对当前顶点的所有邻接点进行迭代
list<int>::iterator i;
for (i = adj[v].begin(); i != adj[v].end(); ++i) {
//如果一个邻接点没有被访问过,递归访问
if (!visited[*i]) {
if (isCyclicUtilDFS(*i, visited, v)) //发现存在环,返回true
return true; //及时退出
}
//这一邻接点不是DFS树上i的父结点,且已经被访问过,则存在环
else if (*i != parent) return true; //返回true
}
return false;
}
//如果无向图中含有环,返回true,否则返回false
bool Graph::isCyclicDFS() {
//标记所有顶点为未访问
bool visited[numVertexes];
for (int i = 0; i < numVertexes; ++i) visited[i] = false;
//对每个未访问过的顶点,调用isCyclicUtil判断环是否存在
for (int u = 0; u < numVertexes; ++u)
if (!visited[u]) //不对已经访问过的顶点进行递归
if (isCyclicUtilDFS(u, visited, -1))
return true;
return false;
}
int main() {
Graph g(5); //5个顶点
g.addEdge(0, 1);
g.addEdge(0, 2);
g.addEdge(1, 2);
g.addEdge(0, 3);
g.addEdge(3, 4);
g.isCyclicDFS() ? cout << "Contains Cycle!\n" : cout << "Doesn't Contain Cycle!\n";
Graph g2(3);
g2.addEdge(0, 1);
g2.addEdge(1, 2);
g2.isCyclicDFS() ? cout << "Contains Cycle!\n" : cout << "Doesn't Contain Cycle!\n";
return 0;
}
运行后,图1有环,图2无环:
上面代码的复杂度和普通遍历一样,时间为 ,空间为
(2) 输出环路
用DFS不只是判断环的存在,还可以输出环路。这需要在前面的代码 isCyclicUtilDFS
做一下小修改,添加一个 parent
数组,记录顶点在深度优先生成树上的 parent
。即可输出环路。
2. BFS
使用BFS,也可以在
(1) 判断环的存在
做法也一样:对每个已经访问过的顶点 v
,如果有一个邻接点 w
,它已经访问过,且 w
不是 v
的 parent
,就说明图中存在环。否则就没有环。
不过和DFS不同的是,DFS中用一个 parent
数组的目的主要是为了输出环,而BFS必须有一个 parent
数组来追溯一个顶点的 parent
,区分邻接点中环中的顶点和遍历过程中的父节点(单纯用 visited
数组无法区分)。这样我们才不会错误判断已访问的 parent
顶点构成环。
...
//迭代算法,存在环时返回true
bool Graph::isCyclicUtilBFS(int v, bool visited[]) {
//定义每个顶点的parent为-1
int parent[numVertexes];
for (int i = 0; i < numVertexes; ++i) parent[i] = -1;
//定义一个BFS使用的queue
queue<int> q;
q.push(v);
visited[v] = true;
while (!q.empty()) {
int u = q.front(); q.pop();
//检测u的全部邻接点v,如果v不是u的parent,但是已经访问过,则存在环
for (auto i = adj[u].begin(); i != adj[u].end(); ++i) {
int v = *i;
if (!visited[v]) { //如果没有访问
visited[v] = true; //标记访问
q.push(v); //进队
parent[v] = u; //记录它的parent
} else if (v != parent[u]) //v是u的邻接点,被访问过,但不是u的parent
return true; //返回true
}
}
return false;
}
bool Graph::isCyclicBFS() {
//标记所有顶点为未访问
bool visited[numVertexes];
for (int i = 0; i < numVertexes; ++i) visited[i] = false;
for (int u = 0; u < numVertexes; ++u)
if (!visited[u] && isCyclicUtilBFS(u, visited))
return true;
return false;
}
...
(2) 输出环路
这里的做法和之前一样,就不多说了。
3. Union-Find
(1) 原理讲解
我们可以用并查集,检测给定的无向图是否存在环。注意,这个方法假定无向图中不包含自环边。
原理是:图的全部顶点都各自属于一个集合;然后依次检查图中的所有边,如果边关联的两个顶点不属于同一个集合,就合并两个顶点所在的集合。若图中存在环,则一定会在检测到某条边的时候,发现它关联的两个顶点早已属于同一个集合。
以下图为例:
有 3
个顶点,3
条边,每个顶点一开始都属于各自的集合:
0 1 2
-1 -1 -1
然后,检测 (0,1)
,发现它们属于不同的集合,就合并:
0 1 2 <----- 1 现在代表subset {0, 1}
1 -1 -1
检测 (1,2)
,发现它们还是不属于同一集合,合并:
0 1 2 <----- 2 现在代表subset {0, 1, 2}
1 2 -1
检测 (0,2)
,发现这两个顶点已经属于同一个集合,它们在无向图中已经连通,但是还加入一条边,就会导致环的出现。
(2) 代码实现
代码实现如下,使用边集数组:
#include <bits/stdc++.h>
using namespace std;
//无向图的边
class Edge {
public:
int src, dest;
};
//边集数组
class EdgeSet {
private:
int V, E;
Edge *edge;
int size; //边的数量
public:
EdgeSet(int V, int E);
~EdgeSet() { delete [] edge; }
void addEdge(int v, int w); //加入边到图中
int find(int parent[], int i);
void merge(int parent[], int x, int y);
bool isCycle();
};
EdgeSet::EdgeSet(int V, int E) {
size = 0;
this->V = V;
this->E = E;
this->edge = new Edge[this->E];
}
void EdgeSet::addEdge(int v, int w) {
edge[size].src = v;
edge[size].dest = w;
++size;
}
//使用并查集判断无向图中的环
//找到这个元素所在的子集
int EdgeSet::find(int parent[], int i) {
if (parent[i] == -1) return i;
return find(parent, parent[i]);
}
//进行两个子集的合并
void EdgeSet::merge(int parent[], int xset, int yset) {
if (xset != yset) parent[xset] = yset;
}
//检查无向图中是否存在环
bool EdgeSet::isCycle() {
int parent[V];
for (int i = 0; i < V; ++i) parent[i] = -1;
//迭代图中所有的边,找到每条边的两个顶点所在的子集
//如果发现一条边,它的两个顶点属于相同子集,就存在环
//否则合并两个顶点的子集
for (int i = 0; i < E; ++i) {
int x = find(parent, edge[i].src);
int y = find(parent, edge[i].dest);
if (x == y) return true;
merge(parent, x, y);
}
return false;
}
int main() {
/*
0
| \
| \
1----2 */
EdgeSet es(3, 3);
es.addEdge(0, 1); //(0,1)
es.addEdge(1, 2); //(1,2)
es.addEdge(0, 2); //(0,2)
es.isCycle() ? cout << "Contains Cycle!\n" : cout << "Contains No Cycle!\n";
return 0;
}
运行,结果如下: