一些概念
连通:无向图中的任意两点都可以互相到达。
强连通:有向图中的任意两点都可以互相到达。
连通分量:无向图的极大连通子图。
强连通分量:有向图的极大强连通子图。
DFS 生成树:对一张图(有向无向均可)进行深度优先遍历得到的生成树。
树边:在 DFS 生成树上的边。
前向边:由子树的根连向子树内的非树边。
返祖边:由结点连向其祖先的边。
横叉边:除上面三种之外的边。
割点:无向连通图删除某个点及其所有连边后,图不再联通,则该点是图的一个割点。
割点(桥):无向连通图删除某条边后,图不再联通,则该边是图的一条割边。
点双联通图:不存在割点的无向联通图。
边双联通图:不存在割边的无向联通图。
点双联通分量:无向连通图的极大点双联通子图。
边双联通分量:无向连通图的极大边双联通子图。
缩点(求强连通分量)
对于结点 (u),记录两个信息 (dfn_u) 和 (low_u)。
(dfn) 表示时间戳,即第几个被遍历到的点。
(low) 表示从当前点开始,经过的边的两个端点均处在未找出的强连通分量中,能到达的最小的时间戳。
在 DFS 的过程中,将经过的点塞进一个栈里面。一旦发现 (dfn_u=low_u) 就一直弹栈直至弹出结点 (u),弹出的这些点就构成了一个强连通分量。
然后考虑如何求出 (low_u),枚举 (u) 的每条出边 ((u,v))。
- 结点 (v) 未遍历过,先递归处理该点,这样 ((u,v)) 就成了树边,然后 (low_ugetsmin{low_u,low_v})。
- 结点 (v) 已遍历过。
- 结点 (v) 处在一个已找出的强连通分量中,根据定义直接跳过。
- 结点 (v) 处在未找出的强连通分量中,这样 ((u,v)) 就成了非树边,同样地,(low_ugetsmin{low_u,low_v})。请注意,这种情况下 (oldsymbol v) 一定能走到 (oldsymbol u)。否则肯定不能走到 DFS 生成树上的 LCA 处,也就先变成强连通分量了,与前提不符。
(low) 数组其实是在找一条向上((dfn) 递减)的路径,而两个强连通分量是不可能有公共点的,所以我们才会有经过边的限制。
就像这样,(1 o 2 o 3 o 4),(4) 经过红色的横叉边到了 (dfn) 更小的点 (3) 上,而 (3) 又可以合并到 (2) 上,(4) 又肯定是从 (2) 下来的,所以 (4) 属于 (2) 的强连通分量。
但是还有一个问题,(low) 数组有时会不能更新完全,怎么办呢?
按照 (1 o 2 o 3) 的顺序走,假设先遍历到了绿色边,再遍历到了红色边,可以发现,(low_3) 没有更新完全的原因是 (low_2) 没有更新完全。所以问题出在已遍历过的情况中。
但其实是没有关系的,(low) 数组的目的仅仅是判断当前强连通分量是否能够向上合并。就算没有更新完全,如果能够向上合并的话,(dfn_{low_u}) 肯定还是小于 (dfn_u) 的。
所以可以将取 (min) 中的 (low_v) 换成 (dfn_v)。
那么算法的正确性就很显然了,在合法的情况下(能够向上合并)尽可能将当前强连通分量扩大。
割点
可以发现对无向图不可能存在横叉边。并且树边和前向边都是返祖边,返祖边仅由它们两种边组成。
随便钦定一个根节点,判断它是否为割点很简单:是否存在两棵及以上的子树。
诶那对每个点都这么做一下不就好了?对不起,每次需要遍历子树,两个儿子可能是连一起的。
同样地,(dfn) 表示时间戳。但 (low) 的定义要换一下,改成最多经过一条非树边或反向经过一条树边,能到达的最小的时间戳。
这么说也许不是很准确,其实就是子树中的点能直接到达的子树外的点的最小时间戳。
因为子树内部可以随便走来走去。
如果存在 ((u,v)) 其中 (u) 是 (v) 的父亲使得 (low_v=low_u) 则说明以 (v) 为根子树中的点如果不通过 (u) 就一定不能向上走了,说明 (u) 是割点。
如果点 (u) 的所有树边都不能说明 (u) 是割点,那么 (u) 一定不是割点。因为子树内所有的点都能走到子树内某些点,然后通过一条边连出去。
所以我们仅仅需要将所有边判断一下。同样地,这里的更新咋写?
树边 (low_u=min{low_v,low_u})?对。
非树边 (low_u=min{low_u,low_v})?错!
请注意这里是割点,一个点被割掉后包含该点的所有边都被删掉了!如果我们恰好经过一条非树边或反向经过一条树边回到了某个割点,然后再经过一条非树边或反向经过一条树边回到更上面的点……显然是不合法的!
所以我们仅能经过一条非树边或反向经过一条树边回上去(把 (low_v) 改成 (dfn_v)),这样就算回到了割点,也不能向上跳了,并不会导致割点的判断错误。而多于一条边就可能出错了!
显然这样 (low) 的更新是完全的。
割边
(low) 的定义和割点稍有不同,不能反向经过树边。
- 首先非树边不可能成为割边。
- 对于一条边 ((u,v)) 其中 (u) 是 (v) 的父亲,如果 (low_v>dfn_u) 则说明 ((u,v)) 是割边。
不取等于是因为 (u) 在子树外。其余原理分析类似,不赘述。
唯一需要注意的是反向经过一条树边的处理。当出现重边时,我们会误判。所以当第二次遇到回到父亲的边时,直接将父亲的 (dfn) 赋给当前的 (low) 即可。
边双联通分量
先讲这个是因为比较简单。
找出所有的割边并割掉,剩下的连通块就是所有的边双联通分量。
点双联通分量
其实就是每次删除割点,分成若干个不连通的子图,然后将割点重新加入每个子图,但不合并子图。如果找不到割点就说明是点双联通分量了。
当然不能直接这么干。在找割点的过程中,将经过的点塞进一个栈里面,一旦发现点 (u) 的儿子点 (v) 满足 (low_v=low_u) 就不断弹栈直到弹出点 (v),再加上点 (u) 就是一个点双联通分量。
一个割点至少存在于两个点双联通分量中。
圆方树
原图中每个点双都对应一个方点,每个点都对应一个圆点,每个方点向其点双中的点对应的圆点连边。
比如说菊花图和链的圆方树分别长这样:
一些性质:
- 因为割点的数量小于 (n),所以圆方树点数小于 (2n)。
- 方点之间不可能存在边,而圆点之间可能存在边。
- 方点的度即点双连通块的大小。