图的遍历
图的遍历方式主要有深度优先遍历和宽度优先遍历,这两种遍历方式借鉴了搜索中的深度优先搜索和宽度优先搜索的思想。
下面简要介绍一下图的遍历方式及图的拓扑排序和可行遍性。
1.图的深度优先搜索
其基本思想是访问顶点V0 ,然后访问V0 邻接到的未被访问的顶点,再从该顶点出发递归地按照深度优先的方式遍历。当遇到一个顶点u,所有邻接于顶点u的点都被访问了时,则回到已访问顶点序列中最后一个拥有未被访问相邻顶点的点w,从w出发继续访问。如此直到所有的顶点都已经被访问过了。由此可见,图的深度优先遍历是沿着图的某一条分支遍历,直到末端,然后回溯,沿着另一分支进行同样的遍历,直到所有的分支都被遍历过为止。
代码如下:
1 //链式前向星存储 2 bool vis[maxn]={0}; 3 4 void dfs(int s) 5 { 6 vis[s]=true; 7 for(int i=head[s];i!=-1;i=edge[i].next) 8 { 9 if(!vis[edge[i].to]) 10 { 11 dfs(edge[i].to); 12 } 13 } 14 }
因为每条边都被处理一次,所以遍历效率是O(m).
2.图的宽度优先遍历
其基本思想是先访问顶点V0 ,然后访问同V0 相邻接的所有顶点V1,……Vi,再依次访问V1,……Vi邻接到的所有未被访问过的节点,以此类推,直到所有顶点都被访问。
代码如下:
1 //链式前向星存储 2 bool vis[maxn]={0}; 3 4 void bfs(int s) 5 { 6 int queue[maxn]; 7 int iq=0; 8 queue[iq++]=s; 9 int i,k; 10 for(i=0;i<iq;i++) 11 { 12 vis[queue[i]]=true; 13 for(k=head[queue[i]];k!=-1;k=edge[k].next) 14 { 15 if(!vis[edge[k].to]) 16 { 17 queue[iq++]=edge[k].to; 18 } 19 } 20 } 21 }
3.图的拓扑排序
图的拓扑排序是对有向无环图而言的。对一个图G进行拓扑排序,是将G中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若图中存在边<u,v>,则u在v之前出现。对有向无环图进行拓扑排序产生的线性序列称为满足拓扑次序的序列,简称拓扑序列。一个有向无环图通常可以表示某种动作序列和方案,而有向无环图的拓扑序列通常表示某种方案切实可行。
无前驱的顶点优先的拓扑排序算法是一种有效的找到拓扑序列的方法。算法如下:
1)从有向图中选择一个入度为0的顶点并输出。
2)从网中删除该顶点,及其发出的全部边。
3)重复1)和2),直到所有的点都被输出。
对于上面的算法,如果最终存在不能剩余的点,则剩余的点和其间的边一定构成环路。
具体实现时,通常用一个队列存储当前发现的入度为0的点,在更新入度的同时更新这个队列。那么如果只是用一个变量来控制队列的头位置而不是真正的删除队头数据(实际上大多数时候都是这么做的),那么队列最终存储的就会是一个完整的拓扑排序序列。
代码如下:
1 //输入数据时统计每个点的入度,并存入indegree数组。 2 int queue[maxn]; 3 int iq=0; 4 for(int i=1;i<=n;i++) 5 { 6 if(indegree[i]==0) 7 { 8 queue[iq++]=i; 9 } 10 } 11 12 for(int i=0;i<iq;i++) 13 { 14 for(k=head[queue[i]];k!=-1;k=edge[k].next) 15 { 16 indegree[edge[k].to]--; 17 if(indegree[edge[k].to]==0) 18 { 19 queue[iq++]=edge[k].to; 20 } 21 } 22 } 23 24 for(int i=0;i<iq;i++)cout<<queue[i]<<''; 25 cout<<endl;
对于上面的办法,如果iq的值小于n,则说明拓扑排序不存在。总的时间复杂度为O(m+n).如果需要找到所有的拓扑排序,可以使用深度优先搜索来解决。
4.图的可行遍性
目前比较常见的是判断一个图是否为欧拉图或者哈密顿图。
4.1欧拉图
图G的一个回路,若他恰通过G中每条边一次,则称该回路为欧拉回路。具有欧拉回路的图称为欧拉图。欧拉回路就是从图上的一点出发,经过所有边必须且只能一次,最终回到起点的路径。
性质:
1)要想一个图是欧拉图,则必须满足两个条件:一、图是连通的,不能有孤立点存在。而、对于无向图来说度数为奇数的点的个数为0;对于有向图来说,每个顶点的入度要等于出度。
2)一笔画问题:就是可以不回到原点,但是依然要经过所有的边必须且只能一次。在这种情况下,对于图G来说,依然要满足图G是连通的,不能存在孤立点。第二个条件是对于无向图来说,度数为奇数的点的个数为2,并且这两个点一定是路径的起点和终点;对于有向图来说,存在两个顶点,其入度不等于出度,其中一个出度比入度大1,为路径的起点,另一点的入度比出度大1,为路径的终点。
总的来说,求解欧拉回路的方法如下:
使用深度优先搜索,如果某条边被搜索到,则标记这条边为已选择,并且即使回溯也不能将当前的边的状态改为未选择。每次回溯时,记录回溯路径。深度优先搜索结束后,记录的回溯路径就是欧拉回路。
使用链式前向星和深度优先搜索实现的具体程序如下:(无向边要存储两次)
1 int ans[maxm]; 2 int ansi=0; 3 bool vis[maxm<<1];//edge 4 void dfs(int now) 5 { 6 int k; 7 for(k=head[now];k!=-1;k=edge[k].next) 8 { 9 if(!vis[k]) 10 { 11 vis[k]=true; 12 vis[k^1]=true;//标记反向的另一条边 13 dfs(edge[k].to); 14 ans[ansi++]=k;//回溯过程记录边 15 } 16 } 17 }
由于每条边一定且只被访问一次,则该算法的时间复杂度是O(m)。
4.2哈密顿图
图G的一个回路,若他通过图G的每个节点一次且仅一次,就是哈密顿回路。存在哈密顿回路的图就是哈密顿图。哈密顿回路就是从一点出发,经过所有的点必须且只能一次,最终回到原点的路径。图中有的边可以不经过,但是不会经过一条边两次。
哈密顿图的判断是一个典型的NP完全问题。
哈密顿图的两个判定条件:
1)充分条件:设图G是一个具有n个顶点的无向连通图,如果G中任意两个不同顶点的度数之和大于等于n,图G就是哈密顿图。
2)必要条件:设图G=<V,E>是哈密顿图,则对于v的任意一个非空子集S,若以|S|表示S中元素的个数,G-S表示G中删除了S中的点及这些点的连边后得到的子图,则W(G-S)<=|S|成立。其中W(G-S)是G-S中的连通分支数。
基本算法伪代码如下:
在以下说明中s为哈密顿回路的起始点,t为哈密顿回路中终点s之前的点。ans[]为最终的哈密顿回路。
①初始化,令s=1,t为s的任意一个邻接点。
②如果ans[]中元素的个数小于n,则从t开始向外扩展,如果有扩展点v,放入ans[]的尾部,并且t=v,并继续扩展。如果无扩展点,进入步骤③。
③将当前的ans[]倒置,s和t互换,从t开始向外扩展,如果有可扩展点,放入ans[]的尾部,并且t=v,并继续扩展。如果无扩展点,进入步骤④。
④如果s和t不相邻,遍历ans[],寻找点ans[i],使得ans[i]与t相连并且ans[i+1]与s相连,将从ans[i+1]到t的部分的ans[]倒置,t=ans[i+1]。如果s与t连通,进入步骤⑤。
⑤如果当前s与t相连,并且ans[]中元素个数等于n,算法结束。否则,如果s与t相连,但是ans[]中的元素个数小于n,则遍历ans[],寻找点ans[i],使得ans[i]与ans[]外的一点相连,则令s=ans[i-1],t=j,将ans[]中s到ans[i-1]部分的ans[]倒置,将ans[]中ans[i]到t的部分倒置,将点j加入到ans[]的尾部,转步骤②。
代码如下:(使用邻接矩阵存储边)
1 inline void reverse(int ans[maxn],int s,int t) 2 { 3 int temp; 4 while(s<t) 5 { 6 temp=ans[s]; 7 ans[s]=ans[t]; 8 ans[t]=temp; 9 s++; 10 t--; 11 } 12 } 13 14 void Hamilton(int ans[maxn],bool map[maxn][maxn],int n) 15 { 16 int s=1,t; 17 int ansi=2; 18 int i,j; 19 int w; 20 int temp; 21 bool vis[maxn]={false}; 22 for(i=1;i<=n;i++)if(map[s][i])break;//找到与s相连的点 23 t=i; 24 vis[s]=vis[t]=true; 25 ans[0]=s; 26 ans[1]=t; 27 while(true) 28 { 29 //从t向外扩展 30 while(true) 31 { 32 for(i=1;i<=n;i++) 33 { 34 if(map[t][i]&&!vis[i]) 35 { 36 ans[ansi++]=i; 37 vis[i]=true; 38 t=i; 39 break; 40 } 41 } 42 if(i>n)break;//找不到可扩展点 43 } 44 //将当前得到的序列倒置,s和t互换,继续扩展。 45 w=ansi-1; 46 i=0; 47 reverse(ans,i,w); 48 temp=s; 49 s=t; 50 t=temp; 51 //从新的t向外扩展 52 while(true) 53 { 54 for(i=1;i<=n;i++) 55 { 56 if(map[t][i]&&!vis[i]) 57 { 58 ans[ansi++]=i; 59 vis[i]=true; 60 t=i; 61 break; 62 } 63 } 64 if(i>n)break;//找不到可扩展点 65 } 66 //如果s和t不相邻,进行调整。 67 if(!map[s][t]) 68 { 69 //取序列中的一点i,使得ans[i]与t相连并且ans[i+1]与s相连 70 for(i=1;i<ansi-2;i++) 71 if(map[ans[i]][t]&&map[s][ans[i+1]])break; 72 //将从ans[i+1]到t部分的ans倒置 73 w=ansi-1; 74 i++; 75 t=ans[i]; 76 reverse(ans,i,w); 77 } 78 //如果当前s与t相连 79 if(ansi==n)return; 80 //当前序列中元素个数小于n 81 for(j=1;j<=n;j++) 82 if(!vis[j]) 83 { 84 for(i=1;i<=ansi-2;i++)if(map[ans[i]][j])break; 85 } 86 s=ans[i-1]; 87 t=j; 88 //将ans[]中s到ans[i-1]部分倒置 89 reverse(ans,0,i-1); 90 ////将ans[]中ans[i]到t部分倒置 91 reverse(ans,i,ansi-1); 92 //将j加入到ans[]的尾部 93 ans[ansi++]=j; 94 vis[j]=true; 95 } 96 }
时间复杂度O(n2).因为总共扩展了n步路径,每步扩展最多枚举所有的节点。
参考文献《图论及应用》哈尔滨工业大学出版社
特此声明:严禁转载。
2014-02-17