2024 年春节集训 _ 第四课 - 强连通分量

强连通分量

相关的定义
  1. 强连通:我们称一个图是强连通的,当且仅当这个图的任意两个点相互可达。
  2. 强连通图:一张强连通的图。
  3. 强连通子图:一个强连通的子图。
  4. 强连通分量 \(\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}\) 节省一些比较冗余的代码,但是要按照下面的要求书写:

  1. 必要地,你需要记录一个临时要用的变量 \(v\) ,方便你的 \(\texttt{while}\) 循环判定。

  2. 还是要记住弹出元素。

我们通常使用手写栈来操作,实现起来会方便不少。

在弹出栈的过程中,你需要即时维护 \(\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}\)

posted @ 2024-01-01 15:37  q(x)  阅读(6)  评论(0编辑  收藏  举报