读《图算法,Robert Sedgewick》笔记 —— DFS特性
DFS(Depth-First Search)就是我们常说的深度优先搜索(深搜)。他的基本搜索方式这里暂不讨论,就《图算法》中对于DFS的特性分析做一下笔记,表达一些个人的观点。
无向图:
首先是DFS所用到的一些表示:我们用两个数组分别记录访问结点的序列和每个结点在搜索树中的父结点,以ord 和 st表示,若
0 1 2 3 4 5 6 7
ord 0 7 1 4 3 5 2 6
st 0 7 0 4 6 3 2 4
则,遍历顺序为0 -> 2 -> 6 -> 4 -> 3 -> 5 -< 4 -> 7 (->表示顺序访问 -<表示回退)
且3与7都是由4向下访问。
对于遍历时的每条路径,我们大致分其为四类,以v为路径起点、w为终点:
- tree link w是未访问过的点 上例中 0-2 6-4 等顺序访问的都是
- parent link st[w]=v 上例中 2-0 4-6 等,与tree link正好相反
- back link ord[w]<ord[v] 上例中可能有5-0 5-4等,即非父结点的回退
- down link ord[w]>ord[v] 上例中可能有4-5 0-5等,与back相反
DFS相关算法:
- 环的检测:没有back link就没有环。
- 简单路径:DFS遍历一次,时间是线性的。
- 简单连通性:DFS遍历,可以用辅助数组标识不同的连通集合。理论上效率比并查集高,因为是线形的,而UFS是在线性的。但UFS提供了可变化的操作,所以除非是巨大的不变的图可以用DFS遍历来求连通性,不然还是用UFS。
- 生成树:通过DFS可以实现。
- 结点搜索:寻找某结点可达的其他结点,可以用DFS解决。
- 二着色、二分、奇数环:三个都是同类问题,二着色和二分是同一问题,保证parent 与该结点颜色不同就行了。奇数环是指在二着色时遇到back与该结点同色,则存在奇数环。
桥问题:
In any DFS tree, a tree edge v-w is a bridge if and only if there are no back edges that connect a descendant of w to an ancestor of w.
也就是说w为根的子树中,如果没有back edge是由该子树中的非根结点到w的父结点的,则w与其父结点的边就是桥。具体实现用一个low数组来记录该结点经过tree edge后遇到的back edge,比如 5 -> 4 -> 3 -> 5,且5的order是1,则5 4 3的low都为1。具体算法和程序见书。
练习 ZJU2588
有向图:
有向图的相关表达:需要维护两个数组pre和post。pre表示访问结点的顺序,post表示该点回退的顺序,如下表:
0 1 2 3 4 5 6 7 8 9 10 11 12
pre 0 9 4 3 2 1 10 11 12 7 8 5 6
post 10 8 0 1 6 7 9 12 11 3 2 5 4
访问大致顺序为:0 -> 5 -> 4 -> 3 -> 2 -< 2 -< 3 -> 11 ->12 -> 9 -> 10 -< 10 -< 9 -< 8 ......
(zju 2615可以用此方法过)
- tree edge 通过一般的递归调用的,类似无向图的tree edge
- back edge pre : v>w post : v<w 从该结点到祖先结点
- down edge pre : v<w post : v>w 从该结点到子孙结点
- cross edge pre : v>w post : v>w 从该结点到无关结点
相关算法:
- 有向环检测:等于判断一个有向图是否是DAG,只要保证没有back edge就可以保证是DAG。
- 单源点的可达性:不能像无向图那样,通过一次DFS得到连通性后就判断与某源点连通的其他点。由于边是有向的,所以连通性的问题复杂了,每次DFS后只能保证根结点对其子树的结点是可达的,而不能保证其他结点之间的可达性。所以解决该问题的思路是对每个结点做DFS,则可以得到每个结点的可达结点。
可达性问题和图传递闭包:
离散数学中教过求图的矩阵的闭包可以得到一个表示图的可达性的矩阵,有一个warshall算法就是用来解决这种问题的。
for( int i=0; i<v ;i++)
for(int s=0; s<v ;s++)
for(int t=0; t<v ;t++)
if( A[s][i] && A[i][t]) A[s][t]=true;
可以将A[s][i]提出t层迭代,以略微提高性能。但总体算法复杂度为O(V^3)
对于带权有向图,类似与 warshall的Floyd算法,可以顺便求一下最短路径。
for( int i=0; i<v ;i++)
for(int s=0; s<v ;s++)
for(int t=0; t<v ;t++)
if( A[s][i] + A[i][t] < A[s][t] ) A[s][t]= A[s][i] + A[i][t];
回顾提到的单源点的可达性分析中,可以通过对每个结点做DFS求可达性,分析一下,对V个结点,图用邻接表表示,则DFS一次复杂度是O(V+E)。总得复杂度O(V(V+E))。我们将复杂度与E挂上勾了,那么在dense graph中,E接近V^2,DFS不会带来任何好处,而对于sparse graph,E远小于V^2,若接近V的话,可以得到近似O(V^2)的性能,是我们渴望得到的性能。所以DFS在对于sparse graph的可达性问题中是很有帮助的。
而考虑对于每个点的DFS是否可以再做优化后,我们可以得到一个不错的算法。下面摘自《图算法》的一个数据研究,满有价值的:
sparse (10V edges) dense (250 vertices)
-------------------------------------------------- ----------------------------------------------------
V W W* A L E W W* A L
----------------------------------------------------------------------------------------------------------------------
25 0 0 1 0 5000 289 203 177 23
50 3 1 2 1 10000 300 214 184 38
125 35 24 23 4 25000 309 226 200 97
250 275 181 178 13 50000 315 232 218 337
500 2222 1438 1481 54 100000 326 246 235 784
----------------------------------------------------------------------------------------------------------------------
Key:
W Warshall's algorithm
W* Improved Warshall's algorithm
A DFS, adjacency-matrix representation
L DFS, adjacency-lists representation
----------------------------------------------------------------------------------------------------------------------
DAG(有向无环图,拓扑结构):
DFS搜索后没有back edge的有向图就是DAG。
拓扑排序:
DFS对有向图搜索中的数组post,就是拓扑排序的倒置(拓扑结构的倒置仍是拓扑结构)。
基于BFS的典型拓扑排序法也能很好的解决拓扑排序的问题。通过每次找出入度最小的点,然后调整该点指向的所有点的入度,再循环。
有一道简单的例题:PKU 1094
DAG的传递闭包:
为了增强对于DAG的求闭包效率,也是因为没有back edge,所以才能用的算法。复杂度为V^2+VX,X是cross edge数量。若没有down edge,则复杂度近似VE,几乎没有增进,因为down edge和cross edge成反比关系,所以当down edge增多时,意味着cross edge较少,可以加速效率。
down edge和cross edge没直接反比关系,可以考虑pre的大小关系,因为tree edge不考虑,而back edge是不会出现在DAG中的。所以down和cross自然就反比了。
伪代码(由于计算过程是使用了以前的计算结果的,所以书中又称DP+DFS):
递归的函数(int 递归结点){
置pre[w]为第n个访问的结点;
对w指向的边遍历v:
若为down edge,continue;
若没访问过v结点,递归v结点;
因为子树v已经求完闭包了,对v指向的边遍历i:
存在v-i的话,插入w-i。
}
强连通子图的算法:
三个线形算法都是很神奇的方法,简单介绍下,具体的还是看书的好:
- Kosaraju's Algorithm 先DFS一次图的逆置(边都反一反),留post数组,可以取postI(postI[post[a]]=a),再按postI从最右开始DFS,对每次遍历的结点置与根相同的id。最后id相同这即强连通。
- Tarjan's Algorithm 只需要修改一下基本的DFS过程,而且只遍历一次(优于Kosaraju),用到一个栈。Tarjan用到了low数组,类似桥的算法。回想桥的算法中用到的low,记录的是该点通过back edge所能回到的最早的遍历点,那么就可以说是个环,那么经过的点必是强连通的。栈用以记录访问过的点,在出现low和pre相同的点时,取出栈中从该点访问过的点,全部置为该强连通子图的点。
- Gabow's Algorithm 思想同Tarjan,不过没有了low数组,取而代之的另一个栈。因为并没必要所有结点的low,在搜索时,一个栈同T的算法保持树的访问顺序,另一个栈开始同样按顺序压结点,但出现back edge,退栈至back点,在递归回到back点时再通过第一个栈将所有的该强连通子图的点标记id。
练习 PKU 2762
重温传递闭包:
我们可以知道,强连通子图是强连通的,也就没必要求它的传递闭包了,所以我们可以先对图做缩点,然后再求闭包。也就引入核心DAG的概念:
- 先找出强连通子图(缩点)
- 建立kernel DAG
- 计算该DAG的传递闭包
算法基本上都是前面几个套一下的,所以不再罗嗦了。此法对于点越多边越密的图越有效。
链接:
ZJU 2588:
http://acm.zju.edu.cn/show_problem.php?pid=2588
ZJU 2615:
http://acm.zju.edu.cn/show_problem.php?pid=2615
PKU 1094:
http://acm.pku.edu.cn/JudgeOnline/problem?id=1094
PKU 2762:
http://acm.pku.edu.cn/JudgeOnline/problem?id=2762