有向图的强连通分量
一些概念
什么是“流图”?
给定有向图
在一个流图
在
流图中的有向边可分为
- 树枝边,即搜索树上的边,
是 的父节点; - 前向边,即搜索树中
是 的祖先节点; - 后向边,即搜索树中
是 的祖先节点; - 横叉边,即除了以上三种情况的边,它一定满足
。
什么是有向图的强连通分量?
首先有个前置概念:强连通图,是指一张有向图中任意两个节点
有向图中的强连通分量(
简单来讲,一个 SCC 就是一个有向图的一个子图,在这个子图中从任意一个节点出发都能到达这个子图中的其他所有节点。
比如:
这张有向图中有
什么是追溯值?
定义
简单来讲,就是在对原有向图进行 dfs 时节点
比如:
从左上角的节点开始进行 dfs,求出这张有向图中所有节点的 dfn 序,标记在点上。
节点
所能到达的节点中 dfn 值最小的,所以
tarjan 算法求 SCC
tarjan 算法基于有向图的深度优先遍历,能够在
思路:
通过定义不难发现:一个环一定是一个强连通图,因此,tarjan 算法就运用了这一点,对于每个点尝试找到与它能构成环的所有节点。
接下来分析每一种边
- 树枝边,作为考虑是否构成环的基础;
- 前向边,因为是由祖先指向子孙,所以一定不会构成环;
- 后向边,非常有用,因为它和搜索树上从
到 的路径构成环; - 横叉边,看情况,如果经过这条横叉边能走到
的祖先节点上,就是有用的。
综上所述,我们应该寻找“后向边”和“横叉边”和“树枝边”构成的环。
tarjan 算法在执行 dfs 时维护了一个栈。当访问到节点
- 搜索树上
的祖先节点,记为 。设 ,若存在后向边 ,则 与 到 的路径一起构成一个环; - 已经访问过的,并且存在一条路径到达
的节点。设 是满足以上性质的节点,若存在横叉边 ,则 、 到 的路径、 到 的路径共同构成一个环。
接着就要用到 SCC 判定法则:若从
详细的证明在 Tarjan 的论文中,这里主要讲一下怎么理解。
当
举个例子:
从
向下搜索,直到无路可走,开始回溯。
先更新
再更新
再更新
发现
然后从
当搜到节点
搜到
无路可走后,开始回溯。
先更新
再更新
再更新
发现
回溯到
然后发现
无路可走了,回溯。
先更新
再更新
发现
至此,已找出原有向图的
根据以上思路可以写出 tarjan 模板:
void tarjan(int u) {
dfn[u] = low[u] = ++tim;
stk[++top] = u, st[u] = true;
for(int i = h[u]; ~i; i = ne[i]) {
int j = e[i];
if(!dfn[j]) {
tarjan(j);
low[u] = min(low[u], low[j]);
}
else if(st[j]) low[u] = min(low[u], dfn[j]); //这里写成 low[j] 也是正确的,但为了方便和双连通分量的联合记忆,故写成 dfn[j](其实这样写也是对的
}
if(dfn[u] == low[u]) {
int y;
scc_cnt++;
do {
y = stk[top--], st[y] = false;
id[y] = scc_cnt;
}while(y != u);
}
}
特别注意:tarjan 求 SCC 中的 low 数组与求 DCC 的 low 数组定义不同。
前者是:在搜索树中
意思是可以走多步到 的点都算数。
后者是:在搜索树中
意思是只能走一步到 的点才算数。
至于为什么,请参考 Tarjan 论文。(因为我确实不会证)
一些例题
题目大意:
给定一张有向图,每个点都有一个权值,求一条路径,使其得到的权值和最大,可多次经过一个点,但只算一次权值。
思路:
因为点权都是非负的,所以我们要尽可能经过尽量多的点,秉承着“既来之,则安之”的观点,我们每到一个点,便一定可以把它所在的 SCC 中的点都给走一遍。所以我们先缩点,在得到的 DAG 上跑最长路即可。
#include <vector>
#include <cstring>
#include <iostream>
using namespace std;
const int N = 10010, M = 200010;
int n, m;
int h[N], e[M], ne[M], idx;
int v[N];
int hc[N], ec[M], w[M], nec[M], idxc;
int stk[N], q[M << 5];
int top, hh, tt = -1;
int dfn[N], low[N];
bool st[N];
int tim, scc_cnt;
int id[N], scc_w[N];
int in_deg[N], out_deg[N];
int dist[N];
vector<int> scc[N];
void add(int a, int b) {
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
void addc(int a, int b, int c) {
ec[idxc] = b, w[idxc] = c, nec[idxc] = hc[a], hc[a] = idxc++;
}
void tarjan(int u) {
dfn[u] = low[u] = ++tim;
stk[++top] = u, st[u] = true;
for(int i = h[u]; ~i; i = ne[i]) {
int j = e[i];
if(!dfn[j]) {
tarjan(j);
low[u] = min(low[u], low[j]);
}
else if(st[j]) low[u] = min(low[u], low[j]);
}
if(dfn[u] == low[u]) {
int y;
++scc_cnt;
do {
y = stk[top--], st[y] = false;
id[y] = scc_cnt;
scc_w[scc_cnt] += v[y];
scc[scc_cnt].push_back(y);
}while(y != u);
}
}
int main() {
memset(h, -1, sizeof h);
memset(hc, -1, sizeof hc);
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i++) scanf("%d", &v[i]);
int a, b;
for(int i = 1; i <= m; i++) {
scanf("%d%d", &a, &b);
add(a, b);
}
for(int i = 1; i <= n; i++)
if(!dfn[i]) tarjan(i);
for(int i = 1; i <= n; i++) {
for(int j = h[i]; ~j; j = ne[j]) {
int y = e[j];
if(id[i] != id[y]) {
addc(id[i], id[y], scc_w[id[y]]);
++in_deg[id[y]];
++out_deg[id[i]];
}
}
}
int ans = 0;
for(int i = 1; i <= scc_cnt; i++) {
if(!in_deg[i])
q[++tt] = i;
dist[i] = scc_w[i];
}
while(hh <= tt) {
int t = q[hh++];
for(int i = hc[t]; ~i; i = nec[i]) {
int j = ec[i];
dist[j] = max(dist[j], dist[t] + w[i]);
in_deg[j]--;
if(!in_deg[j]) q[++tt] = j;
}
}
for(int i = 1; i <= scc_cnt; i++)
if(!out_deg[i]) ans = max(ans, dist[i]);
printf("%d\n", ans);
return 0;
}
P2341 [USACO03FALL / HAOI2006] 受欢迎的牛 G
题目大意:
给定一张有向图,问有多少个所有点都能到达的点?
思路:
根据 SCC 的定义可知:在一个 SCC 内所有点都是互相可达的,所以不妨先缩点,然后整张图就变成了一个 DAG,若这个 DAG 中出度为
#include <cstring>
#include <iostream>
using namespace std;
const int N = 10010, M = 50010;
int n, m;
int h[N], e[M], ne[M], idx;
int hc[N], ec[N], nec[N], idxc;
int stk[N];
int tt;
int dfn[N], low[N];
bool st[N];
int tim, scc_cnt;
int id[N], scc_siz[N];
int deg[N];
void add(int a, int b) {
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
void tarjan(int u) {
dfn[u] = low[u] = ++tim;
stk[++tt] = u, st[u] = true;
for(int i = h[u]; ~i; i = ne[i]) {
int j = e[i];
if(!dfn[j]) {
tarjan(j);
low[u] = min(low[u], low[j]);
}
else if(st[j]) low[u] = min(low[u], dfn[j]);
}
if(dfn[u] == low[u]) {
int y;
++scc_cnt;
do {
y = stk[tt--], st[y] = false;
id[y] = scc_cnt;
++scc_siz[scc_cnt];
}while(y != u);
}
}
int main() {
memset(h, -1, sizeof h);
memset(hc, -1, sizeof hc);
scanf("%d%d", &n, &m);
int a, b;
for(int i = 1; i <= m; i++) {
scanf("%d%d", &a, &b);
add(a, b);
}
for(int i = 1; i <= n; i++)
if(!dfn[i]) tarjan(i);
for(int i = 1; i <= n; i++) {
for(int j = h[i]; ~j; j = ne[j]) {
int y = e[j];
if(id[i] != id[y]) deg[id[i]]++;
}
}
int ans = 0, cnt = 0;
for(int i = 1; i <= scc_cnt; i++)
if(!deg[i]) cnt++, ans += scc_siz[i];
if(cnt == 1) printf("%d\n", ans);
else puts("0");
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!