图的连通性

无向图的深度优先搜索

深度优先搜索的算法过程

在图上做DFS时,我们从某个点出发,递归地访问所有与该节点有边相连的节点。在这个过程中,我们用数组vis记录下每个点是否被访问过,在每次访问相邻节点的时候只访问那些没有被访问过的,由此来保证每个节点只被访问一次。如果我们不停找没有被访问过的节点进行DFS,直到所有节点都被访问,我们发现这个过程中每个点恰好被访问一次,而每条边恰好被访问两次(分别被边所连接的节点访问一次),因此进行一次完整的DFS的复杂度是\(O(|V|+|E|)\)

直觉就可以发现,对某个点出发DFS之后被访问到的节点恰好就是与这个点相连通的所有点。我们可以通过归纳法严格地证明每个与该点连通的点都会被访问,以及每个被访问的点都与该点连通:如果该点被访问,那么“恢复出递归的过程”就可以找到这样一条路径;如果到终点有路径却没有被DFS访问,那么所有指向终点的点也都没被访问,所有指向指向终点的点的点也没被访问,最后发现起点指向的点也没被访问,矛盾。

DFS树

任何一个递归的过程都可以找到一个“树”来与它同构地对应。在无向图的DFS中,对应的就是“DFS树”。在DFS树上,节点就是图的节点,而树边恰好是那些“被我们用来到达这些节点”的边。所以DFS树是原图的一颗生成树,而那些“非树边”横跨在树上的节点之间,它们之所以没能成为树边是因为它们指向的节点在DFS的过程中已经被“事先访问过”了。

更深入的研究DFS树会给我们带来很多的发现。为了更好的看清DFS树的结构,我们在算法的过程中用一个变量\(t\)来“计时”。当我们第一次到达某个节点\(i\)的时候,我们取出\(t\)的值记为\(pre[i]\),并让\(t\)++。而当\(i\)的所有递归都完成时,我们再次取出\(t\)的值,记为\(post[i]\),并也让\(t\)++。这样,每个点的\([pre[i],post[i]]\)就形成了时间上的一段区间,DFS树上的每个节点的区间都恰好包含住了它的各个儿子节点的区间(这让我联想到线段树,但是是多叉的线段树;也让我想到几何关系的偏序集)。

有向图的深度优先搜索

边的分类

相对于无向图来说,从有向图上某点出发DFS所能到达的节点是哪些,是不太直观的。尽管如此,“所有被访问的节点恰好是所有从该点出发有路径能到达的节点”这个结论依然是成立的(证明和无向图中相同,事实上那个证明在有向图中更有意义),并且我们依然在这些节点上画出DFS树。此时,我们有必要对非树边进行更精细的分类:有一类边从一个节点向下指向它的某个后代节点,称为forward edge;另一类边从某个几点指向它的某个祖先节点,称为把back edge;还有一类边横跨两棵子树,即连接着两个自身不作为LCA的节点,称为cross edge。如果在DFS的过程中计时,时间区间依然满足无向图中的那种包含的性质。

有向图有环当且仅当DFS树上存在back edge。如果存在back edge,那么在DFS上一定存在back edge的端点形成的链,它们与back edge一起形成了一个环。如果有环,那么假设环上的某个点是DFS过程中第一个被探测到的,根据DFS的过程我们将会探测到环上的所有其它点,也即换上的所有点一定在DFS树上以起点为根的子树上,这样环上指向这个起点的就一定是一条back edge。

DAG,拓扑排序

如果有向图上没有环,那么这张图就被称为有向无环图(DAG)。DAG很像被“压缩起来的树”,因为它看上去反映了节点之间的某种顺序结构(再一次,我联想到偏序集)。为此,我们想给所有点排序,形成排列\(\sigma\),要求任意\(i<j\),如果\(\sigma_i,\sigma_j\)间有路径可达,就必须是\(\sigma_i\)出发到达\(\sigma_j\)(由于没有环,这就意味着\(\sigma_j\)出发一定无法到达\(\sigma_i\))。从偏序集的角度来看,要求任何两个可比较偏序关系的节点,小的一定要出现在大的前面。DAG节点的这种顺序称为“拓扑序”,这个过程也称为图的“线性化”。

我们已经研究清楚了树边上\(pre,post\)的关系,这个关系可以表达为对于任何tree edge或forward edge \(u \to v\),一定有\(pre[u]<pre[v]<post[v]<post[u]\)。而对于cross edge 或 back edge \(u \to v\)\(u,v\)之间没有包含关系了。联想线段树的结构,两个时间区间不是包含就是无交,不可能是“相交”的。此时一定有\(v\)\(u\)先访问,因此得出关系\(pre[v]<post[v]<pre[u]<post[u]\)

于是我们观察到,无论是何种边,DAG中任意一条边\(u \to v\)始终有\(post[v] <post[u]\)成立。如果我们不断做DFS直到访问图上的所有点,在这个过程中\(t\)始终不清零而是继续累加,那么以上关系(边的\(post\)关系)依然成立。因此\(post\)实际上就是我们所要的拓扑序!只不过在拓扑序中,我们宁愿让\(v\)的排名靠后,所以我们把\(post\)倒过来,得到结论:DAG的拓扑序是\(post\)序的倒序。

根据这个结论也可以用另一种不基于DFS的算法来做拓扑排序:如果我们有一种快速的做法找到DAG中\(post\)最大的点,然后把它连通与它相连的边从图中删去,递归地求解余下的图的拓扑序,我们也能求出整张图的拓扑序。这里要注意,不同的DFS方式(例如在选择一个点的相邻边的时候的顺序)会产生不同的DFS树,因此也就会产生不同的\(post\)序。DFS树从来都不是唯一的,从而拓扑序也不是唯一的。我们断言,当一个点的入度为0就一定可以作为\(post\)最大的节点。设这个点为\(u\),我们可以通过上帝视角先从\(u\)出发DFS,能访问到的点集记为\(S\)。当我们真正开始DFS的时候,我们先从所有\(S\)以外的点出发。我们能保证这个过程中我们永远不能到达\(u\),因为根本没有边会是指向\(u\)的。因此到最后,我们就从\(u\)出发DFS,这样就能保证\(u\)一定是\(post\)最大的节点。这样我们就证明了,任何一个入度为0的点都可以作为某个DAG拓扑序中排名第一的节点。

\(post\)最大的点一定是入度为0的点。如果它入度不为0,那么如果指向它的点之前没有被DFS过,那么还要继续DFS,与它的\(post\)最大矛盾;要么指向它的点之前被DFS过,这意味着它在访问结束后还要返回上一层,因此也与它的\(post\)最大矛盾。

还要补充说明一点,任何一张DAG上一定存在入度为0的点。我们可以这样想象:从某个点出发,如果它入度不为0,就沿着这条入边到达那个节点。不断重复这个过程,要求不能访问重复的点。如果某一时刻入度为0了,那么就结束;如果某一时刻入度不为0但所有想去的节点都已经被访问过了,这说明一定出现了环,矛盾;如果永远没有碰到以上两种情况,我们就会访问无穷个节点,这与点的个数有限矛盾了。同样的也可以证明,任何一张DAG上也一定存在出度为0的点。

强连通分量

为了更好地看清有向图中的连通性关系,我们在有向图中寻找像无向图的连通块那样的“两两可达的”点的集合。如果有向图中某个这样的点的集合达到了极大,即再往里面加任何一个点都将破坏这种“两两可达”性,就称这个点集为一个“强连通分量”。每个点都归属于某个强连通分量,如果非常不幸,这个点可能仅由它自己就构成了一个强连通分量。由此,我们来试图改造有向图的相貌。如果我们把每个强连通分量中的点缩成一个点,这样强连通分量内部的边就消失了,余下的边让他们对应地连接相应的强连通分量,那么我们就得到了一张新的图。一个非常重要的事实是,这张图里一定没有环了!如果有,那么这个环上的任意两个“点”之间两两可达,而“点”作为原图中的强连通分量内部一定两两可达,这就意味着所有这些点的集合在原图中两两可达了,这就和强连通分量的“极大”矛盾了。因此,我们得到的这张新图是一张DAG!至此,有向图的连通性已经被表现得非常清晰了。

那么如何找强连通分量?假设我们有上帝视角,已经把这张缩点后的DAG画出来了。这张DAG上有一些出度为0的“点”。从这个“点”内部的某个原图中的点出发DFS,由于有向图“DFS访问到的节点恰好是所有有路径可达的点”这一性质,我们访问到的节点恰好就是这整个强连通分量。那么不断地删去DAG上出度为0的强连通分量,我们就能找出所有强连通分量。那么唯一的问题是,如何在原图上找到这个“出度为0的强连通分量”。

一种算法是Kosaraju算法。我们已经知道,在DAG中\(post\)最大的点一定是入度为0 的点。这个结论对于缩点后的DAG也是成立的——即\(post\)最大的点一定位于入度为0的“强连通分量”中(证明方法完全相同)。而我们要找的出度为0的强连通分量。所以我们可以建一个“反图”——把所有边反向。边的反向并不会改变强连通分量,因此只需把缩点后的DAG的所有边也反向就会得到这个反图的DAG。在反图中DFS,\(post\)最大的点所在的强连通分量是反图里入度为0的,因此就是原图中出度为0的!在原图\(G\)中从这个点出发DFS,所得到的点集恰好就是我们要找的连通分量了。然后把它删除,重复上面的操作,就可以找出所有的强连通分量。注意到,删除一部分点之后,剩下点之间\(post\)值的相对顺序并没有改变,它依然与某一种DFS序相对应,因此我们继续找\(post\)最大的点依然是可行的。事实上,我们没有必要每次“找”\(post\)最大的点,只需要在反图DFS的过程中按\(post\)顺序记下节点,然后在原图中按照从大到小的顺序选点开始DFS就行了。

另一个算法是Tarjan算法。由于有向图DFS树中back edge和cross edge的存在,某个点出发能够走到\(pre\)值比它小的点。我们记录下每个点所能到达的\(pre\)最小的节点。我们在DFS过程中让每个已经完成递归的节点入栈。我们在缩点后的DAG上想一想,当我们第一次DFS过程中如果某节点在递归完毕(特别注意是完毕,所以我们思考的顺序是从叶子到根)后发现它所能到达的\(pre\)最小的节点就是它自己,那么当前栈内的节点就恰好是一个完整的出度为0的强连通分量!我们让这些节点出栈,并且由于我们不会再访问这些节点,这一部分就好像从原图中消失了一样。所以我们继续上述过程,每一次都好像是在一张新图中“第一次”发现了出度为0的强连通分量一样。我们通过递归模拟出了“把已经找到的出度为0的连通分量从原图中删掉”的效果,由此一步一步找出了所有的强连通分量。

posted @ 2023-03-25 21:52  DennyQi  阅读(87)  评论(0编辑  收藏  举报