强连通分量
概念
连通 在有向图中存在\(u\)到\(v\)的路径,则称\(u\)可达\(v\)。如果\(u,v\)互相可达,则\(u,v\)连通。
强连通 有向图\(G\)强连通指\(G\)中任意两个结点连通。
强联通分量 有向图的极大强连通子图。
DFS 生成树
在有向图上进行 DFS 会形成森林。DFS会形成4 种边。
- Tree Edge,树边
- Back Edge,返祖边,指向祖先结点的边
- Cross Edge,衡叉边,指向搜索过程中已经访问过的结点,但是这个结点并不是祖先节点
- Forward Edge,前向边,指向子孙节点的边
如果结点\(u\)是某个强连通分量在搜索树中遇到的第一个结点,那么这个强连通分量的其余结点肯定是在搜索树中以\(u\)为根的子树中。结点\(u\)被称为这个强连通分量的根。
Tarjan
在 Tarjan 算法中为每个结点维护以下几个变量
dfn[i]
,节点\(i\)的dfs序low[i]
,\(i\)所在子树的节点经过最多一条非树边能到达的结点中最小的 dfs序scc[i]
,节点\(i\)所在的联通块inStk[i]
,节点\(i\)是否在栈中
同时还要维护capacity[i]
表示,强连通分量\(i\)内结点数量
如果出现了low[x] == dfb[x]
的情况,则\(x\)是他所在强连通分量的根,且栈中\(x\)以上的点全部都是这个强连通分量内的结点。
int cnt = 0, sc = 0; // cnt 记录 dfs 序 , sc 记录 强连通分量编号
stack<int> stk;
vector<int> dfn, low, inStk, scc, capacity;
void tarjan(int x) {
low[x] = dfn[x] = ++cnt;
inStk[x] = 1, stk.push(x);
for (auto y: e[x]) {
if (!dfn[y])
tarjan(y), low[x] = min(low[x], low[y]);
else if (inStk[y])
low[x] = min(low[x], dfn[y]);
}
if (low[x] == dfn[x]) {
sc++, capacity.push_back(0);
for (int y; true;) {
y = stk.top(), stk.pop();
inStk[y] = 0, scc[y] = sc, capacity[sc]++;
if (x == y) break;
}
}
}
// 因为有向图搜索会形成森林
for (int i = 1; i <= n; i++)
if (!dfn[i]) tarjan(i);
缩点
求出强连通分量后还可以进行缩点,即把同一个强连通分量的点当做一个点来处理。这样可以得到一个有向无环图。注意因为编号较小的点不能到达编号较大的点,所以 scc 的编号顺序还符合拓扑序(编号越大拓扑序越靠前)。
for( int x = 1 ; x <= n ; x ++ )
for( auto y : e[x] )
if( scc[x] != scc[y] ) g[scc[x]].push_back( scc[y] );
luogu P2341
反向建边,求出强连通分量,缩点后判断找入读为 0 的点,如果不唯一则无解,反之入读为零的点的大小就是答案。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
vector<vector<int>> e, g;
int cnt = 0, sc = 0;
stack<int> stk;
vector<int> dfn, low, inStk, scc, capacity;
void tarjan(int x) {
low[x] = dfn[x] = ++cnt;
inStk[x] = 1, stk.push(x);
for (auto y: e[x]) {
if (!dfn[y])
tarjan(y), low[x] = min(low[x], low[y]);
else if (inStk[y])
low[x] = min(low[x], dfn[y]);
}
if (low[x] == dfn[x]) {
sc++, capacity.push_back(0);
for (int y; true;) {
y = stk.top(), stk.pop();
inStk[y] = 0, scc[y] = sc, capacity[sc]++;
if (x == y) break;
}
}
}
int32_t main() {
ios::sync_with_stdio(false) , cin.tie(nullptr);
int n, m;
cin >> n >> m;
e.resize(n + 1);
dfn.resize(n + 1), low.resize(n + 1), inStk.resize(n + 1), scc.resize(n + 1);
capacity.push_back(0);
for (int x, y; m; m--)
cin >> y >> x, e[x].push_back(y);
for (int i = 1; i <= n; i++)
if (!dfn[i]) tarjan(i);
cout << capacity[sc];
return 0;
}
luogu P2746
首先对图进行缩点,然后求出入度为零的点的数量\(T\),出度为零的点的数量\(S\),任务\(A\)答案就是\(T\),任务B答案就是\(\max( S , T )\)
注意特判整张图本身就强连通的情况。
#include <bits/stdc++.h>
using namespace std;
vector<vector<int>> e;
int cnt = 0, sc = 0;
stack<int> stk;
vector<int> dfn, low, inStk, scc, capacity;
void tarjan(int x) {
low[x] = dfn[x] = ++cnt;
inStk[x] = 1, stk.push(x);
for (auto y: e[x]) {
if (!dfn[y])
tarjan(y), low[x] = min(low[x], low[y]);
else if (inStk[y])
low[x] = min(low[x], dfn[y]);
}
if (low[x] == dfn[x]) {
sc++, capacity.push_back(0);
for (int y; true;) {
y = stk.top(), stk.pop();
inStk[y] = 0, scc[y] = sc, capacity[sc]++;
if (x == y) break;
}
}
}
int32_t main() {
ios::sync_with_stdio(false), cin.tie(nullptr);
int n;
cin >> n;
e.resize(n + 1);
dfn.resize(n + 1), low.resize(n + 1), inStk.resize(n + 1), scc.resize(n + 1);
capacity.push_back(0);
for (int i = 1; i <= n; i++)
for (int y;;) {
cin >> y;
if (y) e[i].push_back(y);
else break;
}
for (int i = 1; i <= n; i++)
if (!dfn[i]) tarjan(i);
if( sc == 1 ){
cout << "1\n0\n";
return 0;
}
vector<int> s(sc + 1, 1), t(sc + 1, 1);
for (int x = 1; x <= n; x++)
for (auto y: e[x])
if (scc[x] != scc[y])
s[scc[x]] = 0, t[scc[y]] = 0;
int S = accumulate(s.begin() + 1, s.end(), 0), T = accumulate(t.begin() + 1, t.end(), 0);
cout << T << "\n" << max(S, T) << "\n";
return 0;
}