Tarjan
求强连通分量
适用于单向边。
维护两个数组 \(dfn_u\) 表示 \(u\) 在搜索树上的访问顺序,\(low_u\) 表示在 \(u\) 的子树里的点经过一条反祖边能到达的点的 \(dfn\) 最小值(只能经过反祖边;但其实经过了前向边是无所谓的,不会影响 \(low_u\) 的值,横叉边是绝对不能经过的)。
当一个点 \(low_u=dfn_u\) 时,他就是某个强连通分量的根。
怎么知道有那些点呢?维护一个栈,每当出现一个强连通分量,就把点出栈直到点为这个强连通分量的根。
其实不难发现,如果点 \(u\) 遍历到的点 \(v\) 还在栈里的话,\(u,v\) 肯定是有祖先关系的,那么 \((u,v)\) 就是一条反祖边或前向边,那么就可以更新 \(low_u=\min(low_u,dfn_v)\)。
如果 \(v\) 在搜索树上是 \(u\) 的儿子,那么有 \(low_u=\min(low_u,low_v)\)。
考虑求出强连通分量后,把一个强连通分量的点缩成一个点。
然后这个图就变成了有向无环图,做拓扑排序即可。
#include <bits/stdc++.h>
void Freopen() {
freopen("", "r", stdin);
freopen("", "w", stdout);
}
using namespace std;
const int N = 2e5 + 10, M = 2e5 + 10, inf = 1e9, mod = 998244353;
vector< int> G[N], scc[N], sta, E[N];
int dfn[N], low[N], vis[N], a[N], col[N], in[N], dp[N];
int sum[N];
int tot, cnt;
void tarjan( int u) {
sta.push_back(u), vis[u] = 1;
dfn[u] = low[u] = ++ tot;
for ( auto v : G[u]) {
if (! dfn[v]) tarjan(v), low[u] = min(low[u], low[v]);
else if (vis[v]) low[u] = min(low[u], dfn[v]);
}
if (dfn[u] != low[u]) return ;
int v; cnt ++;
do {
v = sta.back(); sta.pop_back(), vis[v] = 0;
scc[cnt].push_back(v);
sum[cnt] += a[v];
} while (v != u) ;
}
int n, m;
signed 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, u, v; i <= m; i ++) {
cin >> u >> v;
G[u].push_back(v);
}
for ( int i = 1; i <= n; i ++)
if (! dfn[i]) tarjan(i);
for ( int i = 1; i <= cnt; i ++)
for ( auto u : scc[i]) col[u] = i;
for ( int u = 1; u <= n; u ++)
for ( auto v : G[u])
if (col[u] != col[v]) E[col[u]].push_back(col[v]), in[col[v]] ++;
queue< int> q;
for ( int i = 1; i <= cnt; i ++) if (! in[i]) q.push(i);
for ( int i = 1; i <= cnt; i ++) dp[i] = sum[i];
while (q.size()) {
int u = q.front(); q.pop();
for ( auto v : E[u]) {
dp[v] = max(dp[v], dp[u] + sum[v]);
if (! -- in[v]) q.push(v);
}
}
int ans = 0;
for ( int i = 1; i <= cnt; i ++) ans = max(ans, dp[i]);
cout << ans;
return 0;
}
割点
适用于双向边。
也是维护两个数组 \(dfn,low\)。
\(dfn_u\) 意义相同,而 \(low_u\) 表示在 \(u\) 的子树里的点不经过搜索树上 \(u\) 的父亲能到达的点的 \(dfn\) 最小值。
如何判断割点呢?
如果 \(u\) 为搜索树的根,如果子树个数大于 \(1\),那么 \(u\) 即为割点。
否则,对于一组父子 \((u,v)\) 并且如果 \(low_v\ge dfn_u\),那么 \(u\) 即为割点。
#include <bits/stdc++.h>
void Freopen() {
freopen("", "r", stdin);
freopen("", "w", stdout);
}
using namespace std;
const int N = 2e5 + 10, M = 2e5 + 10, inf = 1e9, mod = 998244353;
vector< int> G[N];
int dfn[N], low[N], cut[N];
int tot, sum;
void tarjan( int u, int fu, int rt) {
dfn[u] = low[u] = ++ tot;
int siz = 0;
for ( auto v : G[u]) {
if (v == fu) continue ;
if (! dfn[v]) {
tarjan(v, u, rt);
siz ++;
low[u] = min(low[u], low[v]);
if ((u == rt && siz > 1) || (u != rt && low[v] >= dfn[u]))
cut[u] = 1;
} else low[u] = min(low[u], dfn[v]);
}
}
int n, m;
signed main() {
ios :: sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> n >> m;
for ( int i = 1, u, v; i <= m; i ++) {
cin >> u >> v;
G[u].push_back(v), G[v].push_back(u);
}
for ( int i = 1; i <= n; i ++)
if (! dfn[i]) tarjan(i, 0, i);
for ( int i = 1; i <= n; i ++) sum += cut[i];
cout << sum << '\n';
for ( int i = 1; i <= n; i ++) if (cut[i]) cout << i << " ";
return 0;
}
点双连通分量
还是和求割点差不多,但是因为割点会处于多个点双,所以出栈时不能把割点出栈了。(所以注意图不连通或多测时栈的清空)。
还有就是注意,有些连通块只有一个点,这也是个点双。
然后就是,求点双时,要假装根节点是一个割点(即使根节点并不是割点),这是因为把根节点看成割点,才能让根节点所在的点双连通分量被判定到。
#include <bits/stdc++.h>
void Freopen() {
freopen("", "r", stdin);
freopen("", "w", stdout);
}
using namespace std;
const int N = 1e6 + 10, M = 2e5 + 10, inf = 1e9, mod = 998244353;
vector< int> G[N];
int low[N], dfn[N];
vector< int> sta, dcc[N];
int cnt, tot;
void tarjan( int u, int fu) {
sta.push_back(u);
dfn[u] = low[u] = ++ tot;
int siz = 0;
for ( auto v : G[u]) {
if (v == fu) continue ;
if (! dfn[v]) {
tarjan(v, u);
siz ++;
low[u] = min(low[u], low[v]);
if (low[v] >= dfn[u]) {
int p; cnt ++;
do {
p = sta.back(), sta.pop_back();
dcc[cnt].push_back(p);
} while (p != v) ;
dcc[cnt].push_back(u);
}
} else low[u] = min(low[u], dfn[v]);
}
if (! fu && ! siz) dcc[++ cnt].push_back(u);
}
int n, m;
signed main() {
ios :: sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> n >> m;
for ( int i = 1, u, v; i <= m; i ++) {
cin >> u >> v;
G[u].push_back(v), G[v].push_back(u);
}
for ( int i = 1; i <= n; i ++)
if (! dfn[i]) {
sta.clear();
tarjan(i, 0);
}
cout << cnt << '\n';
for ( int i = 1; i <= cnt; i ++, cout << '\n') {
cout << dcc[i].size() << " ";
for ( auto u : dcc[i]) cout << u << " ";
}
return 0;
}
割边 & 边双连通分量
这两个放一起说了,因为求了割边就可以直接求边双连通分量。
适用于双向边。
\(dfn,low\) 定义和割点的相同。
对于一组父子 \((u,v)\) 如果 \(low_v > dfn_u\) 则边 \((u,v)\) 为割边。
那么怎么求边双连通分量呢?
先求一次割边,然后把这些边割掉,剩下的若干个连通块就是边双连通分量。
为什么可以呢?因为一个点只会出现在一个边双里。
#include <bits/stdc++.h>
void Freopen() {
freopen("", "r", stdin);
freopen("", "w", stdout);
}
using namespace std;
const int N = 2e6 + 10, M = 2e5 + 10, inf = 1e9, mod = 998244353;
vector< pair< int, int> > G[N];
vector< int> bcc[N];
int cut[N * 2], dfn[N], low[N];
int tot, cnt;
void tarjan( int u, int fe) {
dfn[u] = low[u] = ++ tot;
for ( auto i : G[u]) {
int v = i.first, e = i.second;
if (e == (fe ^ 1)) continue ;
if (! dfn[v]) {
tarjan(v, e);
low[u] = min(low[u], low[v]);
if (low[v] > dfn[u]) cut[e] = cut[e ^ 1] = 1;
} else low[u] = min(low[u], dfn[v]);
}
}
int vis[N];
void dfs( int u, int col) {
if (vis[u]) return ;
vis[u] = 1, bcc[col].push_back(u);
for ( auto i : G[u]) {
int v = i.first, e = i.second;
if (cut[e]) continue ;
dfs(v, col);
}
}
int n, m;
signed main() {
ios :: sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> n >> m;
for ( int i = 1, u, v; i <= m; i ++) {
cin >> u >> v;
G[u].push_back({v, 2 * i}), G[v].push_back({u, 2 * i + 1});
}
for ( int i = 1; i <= n; i ++)
if (! dfn[i]) tarjan(i, 0);
for ( int i = 1; i <= n; i ++)
if (! vis[i]) dfs(i, ++ cnt);
cout << cnt << '\n';
for ( int i = 1; i <= cnt; i ++, cout << '\n') {
cout << bcc[i].size() << " ";
for ( auto u : bcc[i]) cout << u << " ";
}
return 0;
}

浙公网安备 33010602011771号