[算法学习笔记] 强连通分量&缩点
DFS生成树
在介绍强连通分量前,我们先来了解一下DFS生成树。
一棵DFS生成树分为树边,前向边,返祖边(一说反向边),横叉边。我们来画图解释一下:
在这棵DFS生成树中,黑色为树边,它是在DFS遍历时获得的,红色为返祖边,顾名思义,从儿子指向父亲或祖先。蓝色为横叉边,它是在搜索的时候遇到子树中的节点的时候形成的。粉色是前向边,它是在搜索的时候遇到子树中的结点的时候形成的。
强连通分量
强连通分量,具有强连通性,极大性。强连通性顾名思义,在一个强连通分量内的节点都是可以直接或间接互相可达的。极大性指一个强连通分量内不能再加入任何一个节点,再加入任何一个节点都不满足强连通性。
强连通分量一般在有向图中讨论,若在无向图中强连通分量就退化成了连通块。
为了更好的理解强连通分量,我们画图举例:
在这张图中,有两个强连通分量,分别是\({1,2,3}\)和\({1,5,6}\)。由此我们发现一个点可以同属于两个不同的强连通分量。
Tarjan
求强连通分量的方法有很多,参照OI-wiki,这里主要介绍最常见的Tarjan算法。
在讲解Tarjan 算法求强连通分量前,我们定义:
- \(dfn_u\) 表示节点\(u\)被dfs遍历时的顺序编号
- \(low_u\) 表示节点\(u\)经过若干条树边,最多经过一条返祖边能到达的\(dfn\)值最小的点
我们发现,在一个强连通分量中,有且只有一个节点的\(dfn\)值等于\(low\)值,这个节点同时也是一个强连通分量中被最先dfs遍历到的点,也就是\(dfn\)值最小的点。我们不妨将这个节点定义为一个强连通分量中的祖先。在接下来的Tarjan过程中会用到这个重要的性质。
\(low\)值可以在dfs的同时得到,对于一条边\(u-v\),如果:
-
\(v\)还未访问,则\(low\)值还不能确定,所以先dfs \(v\),再用\(low_v\)更新\(low_u\),因为\(u,v\)是一条边,\(v\)能到达的点\(u\)也一定可以。
-
\(v\)已经访问,且\(v\)能到达\(u\),我们发现如果出现这种情况若可以更新\(low_u\)则\(u-v\)是返祖边,故用\(dfn_v\)更新\(low_u\)。
-
\(v\)已经访问,但\(v\)不能到达\(u\),不做处理。
那么我们如何确定\(v\)能否到达\(u\)呢?
容易发现需要判定\(v\)能否到达\(u\)的时候属于上文第二种情况,即出现返祖边。我们可以在dfs的时候维护一个栈,每次dfs将当前节点压入栈,这样判定\(v\)能否到达\(u\)的时候我们只需要判定\(u\)是否在栈中就可以了。
我们现在求出了每个节点的\(low\)值,如何求强连通分量呢?
这里就用到了前面所提到的性质,在一个强连通分量中,有且仅有一个节点的\(dfn\)值等于\(low\)值,这个节点也是一个强连通分量中\(dfn\)值最小的点,我们称之为一个强连通分量的祖先。因此,在回溯的时候只需要判断该节点的\(dfn\)值是否等于\(low\)值,如果相等则证明该节点是一个强连通分量的祖先,那么在栈中祖先节点以上的点都属于该强连通分量,循环出栈,统计答案+1即可。
强连通分量一般和缩点连用,由于强连通分量的极大性,若对强连通分量进行缩点,显然变成了有向无环图。(若有环则不满足极大性,也就是说两个强连通分量还能合并成一个新的强连通分量,显然不可)。
缩完点后的强连通分量由于变成了DAG,所以可以dp,可以最短路。
这里提供一下我的板子 由于我有重度STL依赖症stack都是用的stl可能很丑,仅供参考,不要学我qwq
void Tarjan(int pos)
{
dfn[pos] = low[pos] = ++cnt;
in_stack[pos] = 1;
s.push(pos);
for(int i=0;i<Edge[pos].size();i++)
{
if(!dfn[Edge[pos][i]])
{
Tarjan(Edge[pos][i]);
low[pos] = min(low[pos],low[Edge[pos][i]]);
}
else if(in_stack[Edge[pos][i]])
{
low[pos] = min(low[pos],dfn[Edge[pos][i]]);
}
}
if(dfn[pos] == low[pos])
{
fa_cnt ++;
while(s.top() != pos)
{
fa_num[fa_cnt] ++;
v[s.top()] = 0;
fa[s.top()] = fa_cnt;
in_stack[s.top()] = 0;
s.pop();
}
s.pop();
fa_num[fa_cnt] ++;
fa[pos] = fa_cnt;
in_stack[pos] = 0;
new_map[fa_cnt][fa_cnt] = 1;
}
}
练习题
A
先放一个板子:Luogu P3387 [模板]缩点
Description
给定一个 \(n\) 个点 \(m\) 条边有向图,每个点有一个权值,求一条路径,使路径经过的点权值之和最大。你只需要求出这个权值和。
允许多次经过一条边或者一个点,但是,重复经过的点,权值只计算一次。
Analysis
首先,题目明确可以多次经过一条边或一个点,只是只进行一个权值计算,这一点非常重要。
我们想想如果重复走更优,是什么情况?没错,环,如果有环的存在我们可以走完一个环,把环上的值全都算上,显然更优。其他情况多次走无意义。
如果没有环,直接dp即可,有环也很容易,我们可以Tarjan 缩点!缩点后原图就变成了一个DAG(有向无环图),然后跑dp记搜就很容易了。
题目分析比较简单,主要考察代码熟练度。
Code
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <stack>
#include <vector>
#define N 10010
using namespace std;
int n,m;
int a[N],sum[N];
vector <int> Edge[N];
vector <int> new_Edge[N];
int dfn[N],low[N],fa[N];
int cnt = 0;
int in_stack[N];
stack <int> s;
int fa_cnt = 0;
int in[N];
int maxn = -1;
int f[N];
void Tarjan(int pos)
{
dfn[pos] = low[pos] = ++cnt;
in_stack[pos] = 1;
s.push(pos);
for(int i=0;i<Edge[pos].size();i++)
{
int noww = Edge[pos][i];
if(!dfn[noww])
{
Tarjan(noww);
low[pos] = min(low[pos],low[noww]);
}
else if(in_stack[noww])
{
low[pos] = min(low[pos],dfn[noww]);
}
}
if(dfn[pos] == low[pos])
{
fa_cnt ++;
while(s.top() != pos)
{
sum[fa_cnt] += a[s.top()];
fa[s.top()] = fa_cnt;
in_stack[s.top()] = 0;
s.pop();
}
s.pop();
sum[fa_cnt] += a[pos];
fa[pos] = fa_cnt;
in_stack[pos] = 0;
}
}
int topsort(int pos)
{
if(f[pos]) return f[pos];
int maxnn = 0;
for(int i=0;i<new_Edge[pos].size();i++)
{
maxnn = max(maxnn,f[new_Edge[pos][i]]);
}
f[pos] = maxnn + sum[pos];
maxn = max(maxn,f[pos]);
return f[pos];
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=1;i<=m;i++)
{
int u,v;
cin>>u>>v;
Edge[u].push_back(v);
}
for(int i=1;i<=n;i++)
{
if(!dfn[i]) Tarjan(i);
}
for(int i=1;i<=n;i++)
{
for(int j=0;j<Edge[i].size();j++)
{
if(fa[i] != fa[Edge[i][j]])
{
bool can_push = true;
for(int k=0;k<new_Edge[fa[i]].size();k++)
{
if(new_Edge[fa[i]][k] == fa[Edge[i][j]])
{
can_push = false;
break;
}
}
if(can_push)
{
new_Edge[fa[i]].push_back(fa[Edge[i][j]]);
in[fa[Edge[i][j]]] ++;
}
}
}
}
for(int i=1;i<=fa_cnt;i++)
{
if(!f[i])
{
topsort(i);
}
}
cout<<maxn<<endl;
return 0;
}
码风奇丑qwq,本人患有重度STL依赖症不要学我qwq
B
以前写过题解,直接上链接吧:Link