关于无向图判断是否存在回路的方法:
第一种是类似有向图拓扑排序的思路:(参考有向图判断回路的解答)
如果存在回路,则必存在一个子图,是一个环。因此该子图中所有顶点入度>=1。 算法:
在有向图中,先找出入度为0的顶点,删除与这个顶点相关联的边(出边),将与这些边相关的其它顶点的入度减1,循环直到没有入度为0的定点。如果此时还有未被删除顶点,则必存在环路,否则不存在环路。
无向图则可以转化为:
如果存在回路,则必存在一个子图,是一个环路。因此环路中所有顶点的度>=2。 算法:
第一步:删除所有度<=1的顶点及相关的边,并将另外与这些边相关的其它顶点的度减一。 第二步:将度数变为1的顶点排入队列,并从该队列中取出一个顶点重复步骤一。 如果最后还有未删除顶点,则存在环,否则没有环。 算法分析:
由于有m条边,n个顶点。如果m>=n,则根据图论知识可直接判断存在环路。
(证明:如果没有环路,则该图必然是k棵树 k>=1。根据树的性质,边的数目m = n-k。 k>=1,所以:m<n)
如果m<n 则按照上面的算法每删除一个度为0的顶点操作一次(最多n次),或每删除一个度为1的顶点(同时删一条边)操作一次(最多m次)。这两种操作的总数不会超过m+n。由于m<n,所以算法复杂度为O(n)
第二种为借助广度优先的策略:
任选一个顶点进行广度优先搜索。由于广度优先的算法本质就是从一个顶点出发将图按距离该顶点的远近层层展开为树形结构,如果存在某个顶点被访问两次表明树形展开层次结构中存在回边,因此则必存在回路。 或者利用BFS,在遍历过程中,为每个节点标记一个深度deep,如果存在某个节点为v,除了其父节点u外,还存在与v相邻的节点w使得deep[v]<=deep[w]的,那么该图一定存在回路;
第三种方法:
遍历一遍,判断图分为几部分(假定为P部分,即图有 P 个连通分量)
对于每一个连通分量,如果无环则只能是树,即:边数=结点数-1
只要有一个满足 边数 > 结点数-1
原图就有环
将P个连通分量的不等式相加,就得到:
P1:E1=M1-1
P2:E2=M2-1
...
PN:EN>MN-1
所有边数(E) > 所有结点数(M) - 连通分量个数(P)
即: E + P > M 所以只要判断结果 E + P > M 就表示原图有环,否则无环.
一种比较直接的思路是:为了要判断是否存在环路,需要在每个新近被加入访问队列的顶点上记录下该顶点的上一步的父顶点是哪一个,除了父顶点外其它相关顶点可以加入广度优先算法的队列,并将所有入队顶点的访问次数加一。如果发现访问次数>1的情况,则存在回路。反之,如果算法结束没有访问次数>1的情况则不存在回路。
实例代码如下:
- #include<iostream>
- #include<malloc.h>
- using namespace std;
- #define maxNum 100 //定义邻接举证的最大定点数
- int visited[maxNum];//通过visited数组来标记这个顶点是否被访问过,0表示未被访问,1表示被访问
- int DFS_Count;//连通部件个数,用于测试无向图是否连通,DFS_Count=1表示只有一个连通部件,所以整个无向图是连通的
- int pre[maxNum];
- int post[maxNum];
- int point;//pre和post的值
- //图的邻接矩阵表示结构
- typedef struct
- {
- char v[maxNum];//图的顶点信息
- int e[maxNum][maxNum];//图的顶点信息
- int vNum;//顶点个数
- int eNum;//边的个数
- }graph;
- void createGraph(graph *g);//创建图g
- void DFS(graph *g);//深度优先遍历图g
- void dfs(graph *g,int i);//从顶点i开始深度优先遍历与其相邻的点
- void dfs(graph *g,int i)
- {
- //cout<<"顶点"<<g->v[i]<<"已经被访问"<<endl;
- cout<<"顶点"<<i<<"已经被访问"<<endl;
- visited[i]=1;//标记顶点i被访问
- pre[i]=++point;
- for(int j=1;j<=g->vNum;j++)
- {
- if(g->e[i][j]!=0&&visited[j]==0)
- dfs(g,j);
- }
- post[i]=++point;
- }
- void DFS(graph *g)
- {
- int i;
- //初始化visited数组,表示一开始所有顶点都未被访问过
- for(i=1;i<=g->vNum;i++)
- {
- visited[i]=0;
- pre[i]=0;
- post[i]=0;
- }
- //初始化pre和post
- point=0;
- //初始化连通部件数为0
- DFS_Count=0;
- //深度优先搜索
- for(i=1;i<=g->vNum;i++)
- {
- if(visited[i]==0)//如果这个顶点为被访问过,则从i顶点出发进行深度优先遍历
- {
- DFS_Count++;//统计调用void dfs(graph *g,int i);的次数
- dfs(g,i);
- }
- }
- }
- void createGraph(graph *g)//创建图g
- {
- cout<<"正在创建无向图..."<<endl;
- cout<<"请输入顶点个数vNum:";
- cin>>g->vNum;
- cout<<"请输入边的个数eNum:";
- cin>>g->eNum;
- int i,j;
- //输入顶点信息
- //cout<<"请输入顶点信息:"<<endl;
- //for(i=0;i<g->vNum;i++)
- // cin>>g->v[i];
- //初始画图g
- for(i=1;i<=g->vNum;i++)
- for(j=1;j<=g->vNum;j++)
- g->e[i][j]=0;
- //输入边的情况
- cout<<"请输入边的头和尾"<<endl;
- for(int k=0;k<g->eNum;k++)
- {
- cin>>i>>j;
- g->e[i][j]=1;
- g->e[j][i]=1;//无向图对称
- }
- }
- int main()
- {
- graph *g;
- g=(graph*)malloc(sizeof(graph));
- createGraph(g);//创建图g
- DFS(g);//深度优先遍历
- //连通部件数,用于判断是否连通图
- cout<<"连通部件数量:";
- cout<<DFS_Count<<endl;
- if(DFS_Count==1)
- cout<<"图g是连通图"<<endl;
- else if(DFS_Count>1)
- cout<<"图g不是连通图"<<endl;
- //各顶点的pre和post值
- for(int i=1;i<=g->vNum;i++)
- cout<<"顶点"<<i<<"的pre和post分别为:"<<pre[i]<<" "<<post[i]<<endl;
- //cout<<endl;
- //判断无向图中是否有环
- if(g->eNum+DFS_Count>g->vNum)
- cout<<"图g中存在环"<<endl;
- else
- cout<<"图g中不存在环"<<endl;
- int k;
- cin>>k;
- return 0;
- }
- /*
- 输入:
- 正在创建无向图...
- 请输入顶点个数vNum:10
- 请输入边的个数eNum:9
- 请输入边的头和尾
- 1 2
- 1 4
- 2 5
- 2 6
- 4 7
- 5 9
- 6 3
- 7 8
- 9 10
- */
注意:有向图不能使用此方法。比如1->2,1-3,2->3,4->5,如果使用上述方法会判定为含有还,但并非如此
第四种方法:
利用深度优先搜索DFS,在搜索过程中判断是否会出现后向边(DFS中,连接顶点u到它的某一祖先顶点v的边),即在DFS对顶点进行着色过程中,若出现所指向的顶点为黑色,则此顶点是一个已经遍历过的顶点(祖先),出现了后向边,若完成DFS后,则图中有回路。
c - 顶点颜色表 c[u]
0 白色,未被访问过的节点标白色
-1 灰色,已经被访问过一次的节点标灰色
1 黑色,当该节点的所有后代都被访问过标黑色
深度优先遍历该图,如果在遍历的过程中,发现某个节点有一条边指向已经访问过的节点,并且这个已访问过的节点不是当前节点的父节点(这里的父节点表示dfs遍历顺序中的父节点),则表示存在环。但是我们不能仅仅使用一个bool数组来标志节点是否访问过。如下图
从节点1开始遍历-接着遍历2-接着遍历3,然后发现3有一条边指向遍历过的1,则存在环。但是回到1节点时,它的另一条边指向已访问过的3,又把这个环重复计算了一次。
我们按照算法导论22.3节深度优先搜索中,对每个节点分为三种状态,白、灰、黑。开始时所有节点都是白色,当开始访问某个节点时该节点变为灰色,当该节点的所有邻接点都访问完,该节点颜色变为黑色。那么我们的算法则为:如果遍历的过程中发现某个节点有一条边指向颜色为灰的节点,那么存在环。则在上面的例子中,回溯到1节点时,虽然有一条边指向已经访问过的3,但是3已经是黑色,所以环不会被重复计算。
下面的代码中visit数组的值分为0 1 2三种状态分别代表白色、灰色、黑色,调用函数dfs可以输出图中存在的所有环,图用邻接矩阵表示,如果两个节点之间没有边则对应的值为INT_MAX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
void
dfsVisit(vector<vector<
int
> >&graph,
int
node, vector<
int
>&visit,
vector<
int
>&father)
{
int
n = graph.size();
visit[node] = 1;
//cout<<node<<"-\n";
for
(
int
i = 0; i < n; i++)
if
(i != node && graph[node][i] != INT_MAX)
{
if
(visit[i] == 1 && i != father[node])
//找到一个环
{
int
tmp = node;
cout<<
"cycle: "
;
while
(tmp != i)
{
cout<<tmp<<
"->"
;
tmp = father[tmp];
}
cout<<tmp<<endl;
}
else
if
(visit[i] == 0)
{
father[i] = node;
dfsVisit(graph, i, visit, father);
}
}
visit[node] = 2;
}
void
dfs(vector<vector<
int
> >&graph)
{
int
n = graph.size();
vector<
int
> visit(n, 0);
//visit按照算法导论22.3节分为三种状态
vector<
int
> father(n, -1);
// father[i] 记录遍历过程中i的父节点
for
(
int
i = 0; i < n; i++)
if
(visit[i] == 0)
dfsVisit(graph, i, visit, father);
}
|
算法时间复杂度也是O(E+V)
后向边:
如果第一次访问(u,v)时v为灰色,则(u,v)为反向边。在对图的深度优先搜索中没有发现
反向边,则该图没有回路。
第五种方法:
在图的邻接表表示中,首先统计每个顶点的度,然后重复寻找一个度为1的顶点,将度为1和0的顶点从图中删除,并将与该顶点相关联的顶点的度减1,然后继续反复寻找度为1的,在寻找过程中若出现若干顶点的度都为2,则这些顶点组成了一个回路;否则,图中不存在回路。
第六种方法:
用BFS或DFS遍历,最后判断对于每一个连通分量当中,如果边数m>=节点个数n,那么改图一定存在回路。因此在DFS或BFS中,我们可以统计每一个连通分量的顶点数目n和边数m,如果m>=n则return false;直到访问完所有的节点,return true。