文章目录

  • 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 访问其邻接点 vu深度优先生成树上是 v 的父结点,然后对 v 访问其邻接点时需要特判 u

Unity 有向无环图 无向图有没有环_无向图


代码如下:

#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无环:

Unity 有向无环图 无向图有没有环_Graph_02

上面代码的复杂度和普通遍历一样,时间为 Unity 有向无环图 无向图有没有环_数组_03 ,空间为 Unity 有向无环图 无向图有没有环_Unity 有向无环图_04

(2) 输出环路

用DFS不只是判断环的存在,还可以输出环路。这需要在前面的代码 isCyclicUtilDFS 做一下小修改,添加一个 parent 数组,记录顶点在深度优先生成树上的 parent 。即可输出环路。


2. BFS

使用BFS,也可以在 Unity 有向无环图 无向图有没有环_数组_03

(1) 判断环的存在

做法也一样:对每个已经访问过的顶点 v ,如果有一个邻接点 w ,它已经访问过,且 w 不是 vparent ,就说明图中存在环。否则就没有环。

不过和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) 原理讲解

我们可以用并查集,检测给定的无向图是否存在环。注意,这个方法假定无向图中不包含自环边

原理是:图的全部顶点都各自属于一个集合;然后依次检查图中的所有边,如果边关联的两个顶点不属于同一个集合,就合并两个顶点所在的集合。若图中存在环,则一定会在检测到某条边的时候,发现它关联的两个顶点早已属于同一个集合

以下图为例:

Unity 有向无环图 无向图有没有环_数组_06

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;
}

运行,结果如下:

Unity 有向无环图 无向图有没有环_无向图_07