前言
网上现存\(60\%\)的文章都有明显的误区,本文章经过多次修改,能保证正确性
本文涉及强连通分量、弱连通分量、割点、割边、边双、点双,属于基本图论范畴
在有着直接关联的基础上又有所不同,本文基于把抽象的数组转换为在图上的意义,旨在让初学者能更轻松地理解并区分差别
为避免各个板子的差别过大,在正确的基础上尽量保证代码的相似性
如果您之前学过,可能与您的定义有所不同,故请在看完每个算法下面的代码后再进行文字阅读
文字中某个词语后出现带圆框的数字,如①②,这些词语将会在文字下方有详细的注释,方便阅读
前置
我们简略地定义\(dfs\)树为遍历路程中路径所组成的一棵树,注意下面说的儿子、叶子节点、子树边界\((\)与子树直接相连的外部节点\()\)等用法都从此基础上得出
如下图及\(dfs\)树,\(3,7\)为\(1\)的儿子,叶子节点为\(2,5,6,8\),\(7\)的子树边界为\(\{1\}(1\)与\(7\)和\(8\)直接相连\()\),如果新加一条边\((3,4)\),则\(7\)的子树边界为\(\{1,3\}\)
有向图
强连通分量
定义:有向图中某个点集中的点互相能到达的分量为强连通分量
为方便理解我们采取归纳法:找到完整强连通分量后立即染色
定义\(dfn_u\):表示\(dfs\)中\(u\)的时间戳;初始化为第几个被遍历到的点。
定义\(low_u\):表示\(u\)能到达且在\(u\)子树边界的未染色的最小时间戳①\((\)设代表该最小时间戳点的点为\(x\),可证明\(x\)一定能与\(u\)组成强连通分量②\()\);初始化为\(dfn_u\)。
①:显然代表该最小时间戳的不为\(u\)的子树\((\)除\(u)\),因为子树内的时间戳\(u\)已经为最小的了。故\(u\)的子树并不影响\(low_u\),真正影响的是\(u\)的子树外,与\(u\)子树有接触,且未染色的。
②:\((\)下图为例\()x\)位于\(f\)的左子树,\(x\)所在完整强连通内所有节点不止在左子树\((\)否则就染色了\()\),\(x\)至少能与\(f\)组成强连通分量。故\(x\)一定能与\(u\)组成强连通分量:\(f\rightarrow u\rightarrow x\rightarrow f\)。
具体做法:在\(u\)的子树遍历完后,\(low_u=dfn_u\)则把栈顶到该点的区间染色\((\)与子树外单向联通,那\(u\)的子树未处理部分与\(u\)组成强连通分量\()\),否则要等回到某个祖先后染色才能分量的完整
code
也可更换第\(7\)行代码为:
else if(visit[v]) low[u]=std::min(low[u],low[v]);
//此时定义low:能与u组成强连通分量(未染色)的最小时间戳
void Tarjan(LL u){
dfn[u]=low[u]=++tim; sta[++top]=u; visit[u]=true;
for(LL i=head[u];i;i