2024 年春节集训 _ 第四课 - 强连通分量
强连通分量
相关的定义
- 强连通:我们称一个图是强连通的,当且仅当这个图的任意两个点相互可达。
- 强连通图:一张强连通的图。
- 强连通子图:一个强连通的子图。
- 强连通分量 \(\mathcal{(} \text{scc} \mathcal{)}\):极大的两连通子图。
求解强连通分量 \(tarjan\) 算法
对于一个点,维护两个量:\(\text{low,dfn}\).
整个过程是在 dfs 中进行的,会生成一颗 dfs 生成树,所以下面的讲述中可能会牵扯到一些树的定义;
我们会制作栈,记录访问的节点,当找到一个 \(\text{scc}\) 的时候将栈弹出元素进行处理。
其中 \(\text{dfn}\) 表示这个点的 \(\text{dfs}\) 访问顺序,\(\text{low}\) 表示这个点最早可以访问到的点,形式化地,这个点可以到达的深度最小的点。
一些显著的特征:当 \(\text{low=dfn}\) 的时候可以找到一个 \(\text{scc}\),顺着栈找就行了。
更加详细地,\(\text{low}\) 小于等于 \(\text{dfn}\) ,因为这个点至少可以访问到自己。而 \(\text{low}\) 可以顺着 \(\text{dfs}\) 迭代。
我们还需要记录一个数组 \(vis[v]\) 表示 \(v\) 元素是否在栈内,方便对 \(low\) 更新处理。
小技巧 $\&$ 经验
-begin line-
有时,在弹出栈的过程当中还可以用 \(\texttt{do-while}\) 节省一些比较冗余的代码,但是要按照下面的要求书写:
-
必要地,你需要记录一个临时要用的变量 \(v\) ,方便你的 \(\texttt{while}\) 循环判定。
-
还是要记住弹出元素。
我们通常使用手写栈来操作,实现起来会方便不少。
在弹出栈的过程中,你需要即时维护 \(\texttt{vis, col}\) 两个变量。
注意不要将 \(if\) 写在 \(\texttt{for}\) 循环里面。以免像丰隽一样调两个小时代码。
各种奇葩错误在这里不再点名批评。
-end line-
下面给出一段应用在 \(P1407\) 中的 \(tarjan\) 代码:
程式
void tj(int u){
dfn[u]=low[u]=++clk;
vis[u]=1, st[++top]=u;
for(int v:g[u]){
if(!dfn[v]) tj(v),low[u]=min(low[u],low[v]);
else if(vis[v])low[u]=min(low[u],dfn[v]);
}if(low[u]==dfn[u]){
++clr;
int v;
do{
v=st[top];
vis[v]=0, col[v]=clr;
--top;
if(w[v]<a[col[v]]) a[col[v]]=w[v],s[col[v]]=1;
else if(w[v]==a[col[v]]) s[col[v]]++;
}while(v!=u);
}
}
典型应用:缩点
可以将处于相同强连通分量的点缩成一点,让原图 \(\mathcal{G}\) 缩成一个 \(DAG:\) \(\mathcal{T}\) 这样做方便于处理一些问题。
程式
namespace Graph{
std::vector<int> G[N],T[N];
int col[N],stack[N],dfn[N],low[N],val[N],a[N];
int f[N];
int top,tot;
int n,m,Cnt;
std::bitset<N> vis;
void Init_Input(void){
n=R(),m=R();
for(int i=1;i<=n;++i)
a[i]=R();
for(int i=1;i<=m;++i){
int ai=R(),aj=R();
G[ai].add(aj);
}
}
void Tarjan(int u){
dfn[u]=low[u]=++tot;
stack[++top]=u;
vis[u]=true;
for(int v:G[u]){
if(!dfn[v]) Tarjan(v),low[u]=std::min(low[v],low[u]);
else if(vis[v]) low[u]=std::min(low[u],dfn[v]);
} if(dfn[u]==low[u]){
++Cnt;
while(stack[top]!=u){
col[stack[top]]=Cnt;
val[Cnt]+=a[stack[top]];
vis[stack[top]]=0;
top--;
}
col[stack[top]]=Cnt;
val[Cnt]+=a[stack[top]];
vis[stack[top]]=0;
top--;
}
}
int ans=0;
void Run(void){
Init_Input();
for(int i=1;i<=n;++i)
if(!dfn[i]) Tarjan(i);
for(int i=1;i<=n;++i){
for(int v:G[i])
if(col[i]!=col[v])
T[col[i]].add(col[v]);
}
for(int i=Cnt;i;--i){
f[i]+=val[i];
ans=std::max(ans,f[i]);
for(int v:T[i]) f[v]=std::max(f[v],f[i]);
}
}
void Output(){ IO::W(ans);putchar('\n'); }
}
int main(){ Graph::Run();Graph::Output(); }
例题:
\(\color{orange}{P2746}\) 缩点计算入度,处理处理推倒推倒就好了。
\(\color{orange}{P2812}\) 上面这道题的加强版,一种思路。
\(\color{orange}{CF999E}\) 还是计算入度,推倒推倒思考思考就好了。
\(\color{orange}{P3627}\) 缩点 \(dp\) 的基础题。在 \(\text{DAG}\) 上面考虑转移,也可以在缩点 \(\text{DAG}\) 上面求最短路。
\(\color{orange}{P2341}\) 经典题目,但是咕了很久。正常缩点,然后判断所点后入度为 \(0\) 的 \(scc\) 的数量,只有一个的话输出 \(sz[scc]\) 就好了,否则答案就是 \(0.\) 因为出度为 \(0\) 的 \(scc\) 没办法喜欢任何一个其他的 \(scc.\)
\(\color{orange}{2194}\) 缩点,价值和就是所有 \(scc\) 中的点权最小值的和,而方案数就是 \(scc\) 中的点权数量之积,是乘法原理。有一个双倍经验 \(\color{orange}{CF427C}\)