斯坦福 算法1 第四周笔记

来自斯坦福网站的Algorithms: Design and Analysis,与目前coursera上的版本内容没有变化,不过时间安排略有不同。

1. GRAPH SEARCH AND CONNECTIVITY

1.1 Graph Search

对图进行搜索有一些动机,比如检测图是否联通,对类似数独的问题进行规划,计算得到图的某个部分等等。

一般化的图搜索有两个目标:1)从起始点遍历所有可到达的点;2)不重复遍历同一个点。(最好复杂度是 O ( m + n ) O(m+n) O(m+n))。它的算法流程可以有如下描述(图G,起始点s):
在这里插入图片描述
通过这样的搜索过程之后,可以有一个命题就是,如果算法结束之后点v被遍历到了,那么点v到起始点s有一条连同的路径(有向或无向)。可以通过递归法或者反证法进行证明(世界上果然只有两种证明题,这也要证?这也能证?):
在这里插入图片描述
对这个一般化的图搜索算法进行实例化,就得到了两个常用的图搜索算法,分别是广度优先搜索与深度优先搜索。
在这里插入图片描述
其中BFS是利用了一个队列结构来实现一层层地搜索,而DFS是利用栈来进行递归搜索。两种算法的复杂度都是 O ( m + n ) O(m+n) O(m+n)的。

1.2 BFS(广度优先搜索)

BFS总的来说有三个特点:1)一层层遍历节点;2)可以获得最短路径;3)可以得到无向图的联通部分。计算复杂度为 O ( m + n ) O(m+n) O(m+n)

算法的思想大概是,将初始节点用队列保存,每次取出队列中的一个节点进行遍历,保存其相邻的未遍历节点到队列中,直到队列为空。算法流程如下:
在这里插入图片描述
命题1)可以遍历所有节点。可以通过BFS是1.1中一般化算法的一个特例得到证明。
命题2) 计算复杂度 O ( m + n ) O(m+n) O(m+n)。从代码可以看出,只需要遍历每个节点与边一次。

应用1)最短路径。目标是计算节点v到起始节点s的路径长度。只需要在BFS的代码中进行稍微改造即可。而同样可以通过归纳法得到证明这么做得到的是最短的路径。
在这里插入图片描述
还有的应用比如计算无向图中的两个节点是否联通,或者得到一个图中的联通子图等等。

1.3 DFS(深度优先搜索)

DFS的特点是每次递归遍历,直到无法进行下去的时候才进行回溯。而且能够1)计算出有向无环图的拓扑排序;2)能够得到有向图的强连通分量。计算复杂度也是 O ( m + n ) O(m+n) O(m+n)

DFS的思想是,用一个栈来保存初始节点,每次取出栈顶的节点进行遍历,然后将该节点所有未遍历节点加入栈中,直到栈为空。下面是递归形式的算法流程
在这里插入图片描述
同样DFS算法遍历过的点v也都有到起始点s的路径,以及每个点和边只遍历一次带来的计算复杂度 O ( m + n ) O(m+n) O(m+n)

1.4 拓扑排序

DFS算法的应用有个很重要的就是拓扑排序。首先定义拓扑排序这个问题:对于有向图G中的n个结点,每个结点都有一个映射 f ( v ) ∈ [ 1 , n ] f(v) \in [1,n] f(v)[1,n],且这个值对于一条有向边(u,v)的两个点来说 f ( u ) &lt; f ( v ) f(u) &lt; f(v) f(u)<f(v)

因此如果想要正确地得到相应的拓扑排序值,就需要在边关系的基础上进行序列赋值。在此之前有一点要明确,那就是对于有环的有向图来说不存在拓扑排序。因为根据拓扑的定义,无法给一个环中的两个节点确定其先后顺序,也就得不到这个映射值。

于是我们有命题:对于无环有向图,可以在 O ( m + n ) O(m+n) O(m+n)复杂度下得到该图的拓扑排序。

同样这里也有要注意的一点,每个无环有向图必然有一个吸收节点(sink node,也就是没有出度的点)。(反证法证明,如果每个点都有出度,那么可以无限转移到下个点,而图中的点只有有限个,所以必然有环)。
在这里插入图片描述
明确了这一点之后我们发现,这个吸收节点的拓扑排序值是最大的,也就是n。假如我们把这个吸收节点以及指向它的边去掉,我们就会再次得到一个或多个吸收节点。递归这个过程对每个节点赋值就完成了拓扑排序。

我们可以通过上述思路直接实现拓扑排序的代码,但是实际上对DFS进行小小的修改即可。
在这里插入图片描述
时间复杂度不再说。而算法的正确性也可以得到很好的证明:
在这里插入图片描述

1.5 Strongly Connected Components

1.5.1 算法

有向图的强联通分量(SCCs),指的是对于有向图的某个部分里,每两个节点u,v都有u到v的路径以及v到u的路径。
在这里插入图片描述
圆圈圈起来的部分就是强联通分量。找出这些强联通分量可以使用DFS来计算。

但是如果直接使用DFS我们会发现,算法并不会在SCC的边界停下,而是直接遍历到另外的结点。

不过有一个算法能够解决这个问题,那就是Kosaraju’s Two-Pass Algorithm。这个算法使用了两次DFS,完美的解决了搜寻强联通分量的问题。这个算法的思路如下:
在这里插入图片描述
也就是先在G的反向图Grev上进行DFS-Loop一次,为每个点打上类似与拓扑排序的值。之后在G上根据拓扑排序值的降序用DFS-Loop遍历每个未遍历的点。每一次遍历的点就代表了一个SCC。

其中DFS-Loop是在DFS算法外层套一个For循环,因为可能DFS无法遍历所有的节点。把两次遍历写成同一个算法,其中s和t在第一次与第二次中有不同的作用:
在这里插入图片描述
假设下图是一个已经被逆转后的图Grev,使用DFS-Loop进行第一次遍历,将每个结点打上类似拓扑排序值的标签。可以自己遍历一次试试,结果只能是最后一项。
在这里插入图片描述
再根据上图得到的类似拓扑排序的值进行降序DFS-Loop,我们发现在原图G上会得到它的三个SCCs:
在这里插入图片描述

1.5.2 算法分析

这个算法看起来很神奇,遍历一遍逆转后的图,再遍历一遍原来的图,就得到了SCCs。但是这个算法的正确性是可以证明的。

看一下上面的图计算的过程:
在这里插入图片描述
我们发现如果将每个SCC看做一个大的结点,那么整个图就会变成一个无环图。为啥会是无环图?因为如果依然是有环图,那就表示大的节点可以构成SCC,也就是说两个SCC可以合并成一个,这与我们每个SCC作为一个大结点矛盾。

那么问题来了,对于逆转前的G与逆转后的Grev,它们内部的SCC是什么关系呢?显然逆转前后SCCs的结点组成是不变的(因为是否逆转不改变环的联通性)。

这里我们设个引理,假定图G中两个相邻的SCC C 1 C_{1} C1 C 2 C_{2} C2,在第一次Grev遍历过后每个节点得到的值满足 m a x v ∈ C 1 f ( v ) &lt; m a x v ∈ C 2 f ( v ) max_{v\in C_{1}}f(v) &lt; max_{v\in C_{2}}f(v) maxvC1f(v)<maxvC2f(v)。于是就有了一个推论,最大的f值必然落在可以看出吸收节点的SCC中。
在这里插入图片描述
其实就相当于是SCC作为节点,而SCC中最大的点的f值作为拓扑排序值。因此Grev中最大的拓扑排序值肯定落在吸收节点中(本来吸收节点应该是拓扑排序最小的,但有个逆向操作)。

而因此在第二次DFS-Loop时,进行的操作是按f值从大到小的点进行遍历,因此每次遍历只会在SCC内部进行,于是每个被遍历的点都会代表一个SCC。
在这里插入图片描述
所以,只要引理正确,那么这个算法的正确性就得到了证明。而引理的证明也很简单。假设一个点v是两个相邻SCC中在第一遍Grev中最先被遍历到的节点,于是就有两种情形,对应两种情形都会得到引理正确的证明:
在这里插入图片描述

感觉中间有了引理之后证明会得到正确的SCC的部分不那么严密,不过我也想不出啥更好的说法了。其实第一次在Grev上进行遍历得到的f值,如果每个SCC把最大的点的f值作为SCC的f值,那么这个f值对于SCC作为结点组成的图而言,就是起的拓扑排序值的作用(反向的)。因此从最大的开始往小的SCC遍历,就相当于每次去掉了吸收节点以及指向吸收节点的边。

总的来说,算法的正确性得到了证明。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值