修改时间:2021.11.15
写在前面
图及其遍历算法的基本概念网上教程有很多,这里不在进行赘述。
本文侧重于算法的实现及其思想的讨论,所有代码均为伪代码,意在能简洁明了的阐述算法。
一、 “原生”dfs
1.分析
深度优先搜索算法需要分别实现以下内容:
1.访问顶点的实现
2.“依次从顶点V的未被访问的邻接点出发进行深度遍历”
其中 2 主要涉及:
a.顶点是否被访问标识;
b.顶点v的各邻接点的求解;
c.“从各邻接点出发深度遍历”的实现。
2.代码实现:
基于上述讨论可得dfs算法描述如下:
void dfs(graph G,int v) //从顶尖V出发对图G进行深度优先搜索遍历
{ int w;
visite(v); //访问顶点v
visited[v]=TRUE; //设置访问标志
w=firstadj(G,v); //求v的邻接点,作为深度遍历的新起点
while(w!=0) //当还有邻接点时
{
if(visited[w]=FALSE) //从没有访问过的邻接点出发深度遍历
dfs(G,w);
w=nextadj(G,v,w); //求下一个邻接点
}
}
//完整遍历图的主算法
void dfs_teavel(graph G)
{ int i;
for(i=1;i<=nodes(g);i++) //初始化各顶点访问标志
visited[i]=FALSE;
for(i=1;i<=nodes(g);i++) //依次选择未被访问过的顶点作为起点来深度遍历
if(visited[i]=FALSE)
dfs(G,i);
}
注:
其中引用了两个求邻接点的函数:
firstadj(G,v)–返回图G中顶点v的第一个邻接点的序号,若不存在,则返回0;
nextadj(G,v,w)–返回图G中顶点v的邻接点中处于邻接点w之后的那个邻接点,若不存在,则返回0;
其中求邻接点个数的函数:
nodes(g)–返回图G中的顶点数;
另外算法中的访问顶点的操作visite(v)对每个顶点执行一次且仅执行一次。其具体执行内容在不同场合可以有不同含义。
3.时间复杂度分析:
整个算法要访问每个顶点,在访问每个顶点后,搜索其邻接点是花费时间最多的部分,并且所需要的时间取决于存储结构。
采用邻接矩阵存储图,对每个顶点来说,搜索其所有邻接点需要搜索矩阵中对应的整个一行,由此可知,算法时间复杂度为O(n²);
采用邻接表存储图,对每个顶点来说,搜索其所有邻接点需要搜索邻接表中对应链表的各结点,虽然各顶点邻接表的长度可能大小不一,但整个图的遍历所搜索的邻接点总数就是邻接点的结点数,也就是图的边数e(在无向图时,是2e),由此可知,算法的时间复杂度是O(n+e)。
二、深度遍历算法的应用
1.设计算法求解无向图 G 的连通分量的个数
分析:
对无向图G来说,选择某一顶点v执行dfs(v),可访问到所在连通分量的所有顶点,故为了遍历整个图而 选择起点的次数 就是图G的连通分量数,而这可通过修改dfs_travel来实现:每调用一次dfs算法计数一次。
代码实现:
其中涉及dfs算法可直接照搬,为了清晰性,故此处省略。
int dfs_teavel(graph G)
{ int i;
int k; //增加变量k用于连通分量的计数
for(i=1;i<=nodes(g);i++) //初始化各顶点访问标志
visited[i]=FALSE;
for(i=1;i<=nodes(g);i++) //依次选择未被访问过的顶点作为起点来深度遍历
if(visited[i]=FALSE)
{
k++; //用k来累计连通分量个数
dfs(G,i);
}
return k;
}
拓展:
以下算法与此算法设计思想相同:
a.判断无向图是否连通
(还可以通过dfs后,遍历visited数组是否都为true)
b. 设计算法判断有向图 G 中顶点 V0 到每一个顶点是否都有路径,若是,返回true,否则返回false。
2.设计算法求出无向图 G 的边数
分析:
在执行dfs(v)时,每搜索到一个邻接点即意味着搜索到一条以v为一个端点的边或弧,故应在dfs算法中计数;
由于遍历算法保证每个顶点都要被访问一次,也就意味着每个顶点都要对应的作为起点调用dfs算法一次,因此每个顶点相关联的边都会被计算在内,。由此导致无向图中每条边被计算两次,故最后边数结果应除以2才是实际边数
代码实现:
void dfs(graph G,int v) //从顶尖V出发对图G进行深度优先搜索遍历
{ int w;
visited[v]=TRUE; //设置访问标志
w=firstadj(G,v); //求v的邻接点,作为深度遍历的新起点
while(w!=0) //当还有邻接点时
{
E++; //此处意味着找到一天边,故累计到变量E中
if(visited[w]=FALSE) //从没有访问过的邻接点出发深度遍历
dfs(G,w);
w=nextadj(G,v,w); //求下一个邻接点
}
}
//完整遍历图的主算法
int Enum(graph G)
{ int i;
int E = 0; //增加全局变量E用于记录整个图中的边数
for(i=1;i<=nodes(g);i++) //初始化各顶点访问标志
visited[i]=FALSE;
for(i=1;i<=nodes(g);i++) //依次选择未被访问过的顶点作为起点来深度遍历
if(visited[i]=FALSE)
dfs(G,i);
return E/2;
}
3.设计算法判别有向图 G 是否是一颗以 v0 为根的有向树
分析:
为了设计算法首先要明确相关概念,按照定义,v0的入度为0,其余各顶点的入度值为1.
即该算法需要对每个顶点判断其入度值是否符合定义的要求
为了实现这一判断,可以从v0出发深度遍历,如果每个顶点仅访问一次且仅访问一次则符合要求,判断成立
代码实现:
void dfs(graph G,int v) //从顶尖V出发对图G进行深度优先搜索遍历
{ int w;
visited[v]=TRUE; //设置访问标志
vnum++; //对访问顶点操作计数
w=firstadj(G,v); //求v的邻接点,作为深度遍历的新起点
while(w!=0) //当还有邻接点时
{
E++; //此处意味着找到一天边,故累计到变量E中
if(visited[w]=FALSE) //从没有访问过的邻接点出发深度遍历
dfs(G,w);
else judge =FALSE; //执行到此,标明顶点w被重复访问,故设置失败标志
w=nextadj(G,v,w); //求下一个邻接点
}
}
BOOL IS_DirectedTree(graph G,int v0)
{ int i;
bool judge=TRUE; //judge用于记录判断的信息,初始情况下假设为true
for(i=1;i<=nodes(g);i++) //初始化各顶点访问标志
visited[i]=FALSE;
int vnum=0; //记录访问过的顶点数
dfs(G,vo); //仅从v0出发
return vnum==n && judge;
}
注:
由于是判断型算法,故将返回值类型定义为BOOL型;
为了在两个函数之间传递判断结果,采用了定义bool行全局变量的方法
☆4.设计算法在图中找出一条包含所有顶点的简单图
分析:
搜索这一路径的过程就是深度优先搜索过程,过程中可能会遇到当前顶点(如顶点2)无邻接点,不能再往下搜索了,需要回溯到上一顶点(顶点1),注意此时需要取消顶点2的访问标志,为了使该顶点可被重新搜索,该操作可以放在dfs算法之后;
搜索成功的条件就是当前路径上顶点数正好等于n,因此在求解过程中需要记录当前路径上的顶点数。
还需将路径上的顶点依次保存到数组(严格说是栈)中
代码实现:
void dfs(graph G,int v) //从顶尖V出发对图G进行深度优先搜索遍历
{ int w;
visited[v]=TRUE; //设置访问标志
num++; //记录路径上的顶点数
A[num]=v0; //保存路径上的顶点
if(num==nodes(g)) //符合条件时输出路径
print(A);
w=firstadj(G,v0); //求v0的第一个邻接点
while(w!=0) //当还有邻接点时
{
if(visited[w]=FALSE) //从不在路径上的的邻接点出发深度遍历
dfs(G,w);
w=nextadj(G,v0,w); //求下一个邻接点
}
}
void Hamilton(graph G)
{ int i;
for(i=1;i<=nodes(g);i++) //初始化各顶点访问标志
visited[i]=FALSE;
int num=0; //记录路径上的的顶点数
for(i=1;i<=nodes(g);i++)
dfs(G,i); //以每个顶点作为起点来搜索路径
}
注:
调用dfs及其调用前的准备工作,其中准备部分的操作与深度优先搜索遍历的准备部分操作相同,不同的是调用操作。
☆5.设计算法判断无向图G中顶点v是否是一个关节点。
(在无向图G中,若删除顶点v及其相关的边后,使得v所在的连通分量被分割为两个或两个以上,则称顶点v为关节点。)
分析:
在深度遍历算法中,如果某顶点被访问了,则不会再访问,并且也不会由此往下进行遍历,从而起到了“删除”的作用;
本算法可先访问顶点v,然后从其一个邻接点出发做一次深搜,再检查是否有v的林及诶单未被访问,若有,则说明是关节点。
代码实现:
void dfs(graph G,int v0)
{ visited[v0]=TRUE;
w=firstadj(G,v0); //求v0的第一个邻接点
while(w!=0) //当还有邻接点时
{
if(visited[w]=FALSE) //从不在路径上的的邻接点出发深度遍历
dfs(G,w);
w=nextadj(G,v0,w); //求下一个邻接点
}
}
BOOL judge(graph G,int v0)
{ int num=0; //全局变量num用于存放不能被访问到的顶点数
for(i=1;i<=nodes(g);i++)
visited[i]=FALSE;
visited[v0]=TRUE; //置v0的访问标志位已访问状态
w=firstadj(G,v0); //求v0的第一个邻接点
if(w!=0)
{ dfs(G,w);
w=nextadj(G,v0,w); //求下一个邻接点
while(w!=0) //当还有邻接点时
{
if(visited[w]=FALSE) //从不在路径上的的邻接点出发深度遍历
num=num+1; //统计v0的未被访问的顶点数
w=nextadj(G,v0,w); //求下一个邻接点
}
}
}
注:
如果给出具体的存储结构,则可以在其存储结构上进行删除。
6.设计算法判断连通的无向图G中顶点v是否满足在删除顶点v及其相关边之后,图G仍然是联通的。
分析:
如果删除v后图仍然连通,则从某顶点出发访问能遍历整个图,即一边访问,一边计数
代码实现:
void dfs(int v)
{ visited[v]=TRUE;
n=n+1; //设置访问标志并计数
w=firstadj(G,v); //求v的第一个邻接点
while(w!=0) //当还有邻接点时
{
if(visited[w]=FALSE) //从不在路径上的的邻接点出发深度遍历
dfs(G,w);
w=nextadj(G,v,w); //求下一个邻接点
}
}
BOOL IsACutPoint(graph G,int v)
{ for(i=1;i<=nodes(G);i++)
visited[i]=FALSE;
visited[v]=TRUE; n=1; //实现“删除”功能
dfs(firstadj(G,v)); //从v的一个邻接点出发遍历
return n!=nodes(G); //依据访问的顶点数放回判断结果
}
注:
注意与5的对比,算法中“删除”功能的实现;
7.设计算法判断顶点vi到vj之间是否存在路径。
分析:
可以从vi开始遍历,只需在遍历中判断当前顶点是否是vj即可
注意flag标志的使用
代码实现:
void dfs(graph G,int u,int v,bool&flag)
{ int w;
visited[v]=TRUE; //设置访问标志
if(u==v) //说明找到了重点
flag=true;
w=firstadj(G,u); //w指向u的第一个邻接点
while(w!=0) //当还有邻接点时
{
if(visited[w]=FALSE) //从没有访问过的邻接点出发深度遍历
dfs(G,w,v,flag);
w=nextadj(G,u,w); //求下一个邻接点
}
}
void dfs_teavel(graph G,int i,int j)
{ int i; bool flag=false; //flag用于记录判断的信息,初始情况下假设为false
for(i=1;i<=nodes(g);i++) //初始化各顶点访问标志
visited[i]=FALSE;
dfs(G,i,j,flag);
return flag;
}
8.设计算法判断无向图G是否是一棵树。
分析:
一个无向图是一棵树的条件为:G必须是无回路的连通图或n-1条边的连通图,
后者更好判断,所以采用深搜,从任一点出发,判断是否能遍历n-1条边(即判断是否有n-1条边),并且是否能遍历所有顶点(即判断是否连通)
代码实现:
void dfs(graph G,int v)
{ int w;
vn++; //顶点自增
visited[v]=TRUE; //设置访问标志
w=firstadj(G,v); //求v的邻接点,作为深度遍历的新起点
while(w!=0) //当还有邻接点时
{
en++; //此处意味着找到一天边,故累计到变量E中
if(visited[w]=FALSE) //从没有访问过的邻接点出发深度遍历
dfs(G,w);
w=nextadj(G,v,w); //求下一个邻接点
}
}
BOOL Is_Tree(graph G)
{ int i,n=nodes(g);
int en=0,vn=0; //en vn分别为边数和顶点数
for(i=1;i<=n;i++) //初始化各顶点访问标志
visited[i]=FALSE;
dfs(G,1); //从顶点1开始遍历
if(vn==n && en==2*(n-1))
return true;
return false;
}
注:
最后的判断条件中,要注意到无向图的每个边会被遍历两次;
写在后面
能力有限,写的算法可能不是最优的,不足之处恳请各位大佬及时指出,谢谢~