强连通分量
给定任意一个有向图,当我们找到它的所有强联通分量,把每个强联通分量都看成一个点,就能得到一个有向无环图(DAG),从而简化问题
err-tag
例题 1
给定一个有向图,判断是否对于任意无向点对 \((x,y)\) 都要么可以从 \(x\) 走到 \(y\),要么可以从 \(y\) 走到 \(x\)。
注意到一个 SCC 可以视为一个节点,剩下部分形成一个 DAG。注意到题目限制等价于 DAG 传递闭包的最长反链等于 \(1\),根据 Dilworth 定理,其最小链覆盖等于 \(1\),即该 DAG 就是一条链。
Tarjan 模板题
Tarjan 算法过程
- 全局定义一个栈 \(s\);
- 递归算法,当递归到 \(u\) 点时候,首先把 \(u\) 进栈,并设 \(low[u]=pre[u]\) ,然后去遍历u的出边到v;
- 如果发现 \((u,v)\) 是树边,那么递归 \(v\) 点,然后从 \(v\) 点递归回来以后,用 \(low[v]\) 去更新 \(low[u]\)。
- 如果发现 \(v\) 还没有被标记归属于某个已知的 SCC,那么 \(u\) 就可能和 \(v\) 在一个 SCC 中,故此时用 \(pre[v]\) 更新 \(low[u]\)。
- 当即将离开u点的时候,发现 \(low[u]=pre[u]\) ,那么 \(u\) 就是这个 SCC 的第一个点,这时候栈里面 \(u\) 后面的所有点都属于这个 SCC。
代码
| #include<iostream>
#include<cstring>
#include<vector>
#include<queue>
#include<stack>
using namespace std;
const int N = 10010;
int n, m, tm, cnt;
int dfn[N], low[N], col[N], inSta[N], vis[N];
vector<int> adj[N];
stack<int> sta;
// 快读
inline int read(){
int x = 0, f = 1;
char ch = getchar();
while(ch < '0' || ch > '9'){
if(ch == '-') f = -1;
ch = getchar();
}
while(ch >= '0' && ch <= '9') x = x * 10 + ch - '0', ch = getchar();
return x * f;
}
// Tarjan
void dfs(int u){
// 入栈
sta.push(u);
inSta[u] = 1;
low[u] = dfn[u] = ++tm;
for(auto v : adj[u]){
if(!dfn[v]){
dfs(v);
low[u] = min(low[u], low[v]);
}
else{
if(inSta[v]){
low[u] = min(low[u], pre[v]);
}
}
}
// 判断是不是强连通分量
if(dfn[u] == low[u]){
// 记录这个节点和这个颜色并出栈
col[u] = ++cnt;
inSta[u] = 0;
// 弹出所有栈顶元素,它们同时也是强连通分量里的节点
while(sta.top() != u){
col[sta.top()] = cnt;
inSta[sta.top()] = 0;
sta.pop();
}
sta.pop();
}
}
int main(){
n = read();
m = read();
for(int i = 1; i <= m; i++){
int u = read();
int v = read();
adj[u].push_back(v);
}
// 要考虑非连通图的情况,循环一遍
for(int i = 1; i <= n; i++){
if(dfn[i] == 0) dfs(i);
}
cout << cnt << endl;
for(int i = 1; i <= n; i++){
if(vis[i] == 0){
for(int j = i; j <= n; j++){
if(col[j] == col[i]){
cout << j << ' ';
vis[j] = 1;
}
}
cout << endl;
}
}
return 0;
}
|
代码
| #include<iostream>
#include<vector>
#include<stack>
using namespace std;
const int N = 2E4 + 10;
int n, m, dt, sccCnt, ans;
int sccNo[N], sz[N], pre[N], low[N];
vector<int> adj[N];
stack<int> sta;
void tarjan(int u){
low[u] = pre[u] = ++dt;
sta.push(u);
for(auto v : adj[u]){
if(pre[v] == 0){
tarjan(v);
low[u] = min(low[u], low[v]);
}
else if(sccNo[v] == 0){
low[u] = min(low[u], pre[v]);
}
}
if(low[u] == pre[u]){
sccNo[u] = ++sccCnt;
while(sta.top() != u){
sccNo[sta.top()] = sccCnt;
sta.pop();
}
sta.pop();
}
}
int main(){
cin >> n >> m;
for(int i = 1; i <= m; i++){
int u, v;
cin >> u >> v;
adj[u].push_back(v);
}
for(int i = 1; i <= n; i++){
if(sccNo[i] == 0) tarjan(i);
}
for(int i = 1; i <= n; i++){
sz[sccNo[i]]++;
}
for(int i = 1; i <= sccCnt; i++){
if(sz[i] > 1) ans++;
}
cout << ans << endl;
return 0;
}
|
结论/引理:
在一个 DAG中,入度为 \(0\) 的点有 \(a\) 个,出度为 \(0\) 的点有 \(b\) 个,那么最少添加 \(max(a,b)\) 条边就可以让该 DAG 变成一个强连通图。
解题思路:
• 用 tarjan 计算强连通分量,缩点
• 在形成的分量图中,尽量添加最少的边,使得新图强连通。设有 \(a\) 个结点入度为 \(0\),\(b\)个结点出度为 \(0\),则 \(max(a,b)\) 是答案。特殊情况,如果新图已经强连通了,答案是 \(0\),需要特判。
代码
| #include<iostream>
#include<stack>
#include<cstring>
#include<vector>
using namespace std;
const int N = 2E4 + 10;
int t;
int n, m;
vector<int> adj[N];
stack<int> sta;
int dt, sccCnt;
int pre[N], low[N], sccNo[N];
void tarjan(int u){
sta.push(u);
low[u] = pre[u] = ++dt;
for(auto v : adj[u]){
if(!pre[v]){
tarjan(v);
low[u] = min(low[u], low[v]);
}
else if(!sccNo[v]){
low[u] = min(low[u], pre[v]);
}
}
if(low[u] == pre[u]){
sccNo[u] = ++sccCnt;
while(sta.top() != u){
sccNo[sta.top()] = sccCnt;
sta.pop();
}
sta.pop();
}
}
int noIn, noOut;
bool ind[N], otd[N];
int main(){
cin >> t;
while(t--){
cin >> n >> m;
sccCnt = dt = 0;
noIn = noOut = 0;
memset(sccNo, 0, sizeof(sccNo));
memset(pre, 0, sizeof(pre));
memset(otd, 0, sizeof(otd));
memset(ind, 0, sizeof(ind));
for(int i = 1; i <= n; i++){
adj[i].clear();
}
for(int i = 1; i <= m; i++){
int u, v;
cin >> u >> v;
adj[u].push_back(v);
}
for(int i = 1; i <= n; i++){
if(!sccNo[i]) tarjan(i);
}
for(int u = 1; u <= n; u++){
for(auto v : adj[u]){
if(sccNo[u] == sccNo[v]) continue;
otd[sccNo[u]] = 1;
ind[sccNo[v]] = 1;
}
}
for(int i = 1; i <= sccCnt; i++){
if(otd[i] == 0) noOut++;
if(ind[i] == 0) noIn++;
}
if(sccCnt == 1) cout << 0 << endl;
else cout << max(noIn, noOut) << endl;
}
return 0;
}
|
解题思路:
在缩点形成的 DAG 中,如果有大于一个点(其实是一个强连通分量)的出度是0,那么就没有任何一头奶牛是明星,因为一个(群)出度为 \(0\) 的奶牛并不喜欢另一个(群)出度为 \(0\) 的奶牛。
代码
| #include<iostream>
#include<stack>
#include<vector>
using namespace std;
const int N = 1E4 + 10;
int n, m;
vector<int> adj[N];
stack<int> sta;
int dt, sccCnt;
int pre[N], low[N], sccNo[N], sccSz[N];
void tarjan(int u){
sta.push(u);
low[u] = pre[u] = ++dt;
for(auto v : adj[u]){
if(!pre[v]){
tarjan(v);
low[u] = min(low[u], low[v]);
}
else if(!sccNo[v]){
low[u] = min(low[u], pre[v]);
}
}
if(low[u] == pre[u]){
sccNo[u] = ++sccCnt;
sccSz[sccCnt]++;
while(sta.top() != u){
sccNo[sta.top()] = sccCnt;
sta.pop();
sccSz[sccCnt]++;
}
sta.pop();
}
}
int otd[N], noOut;
int main(){
cin >> n >> m;
for(int i = 1; i <= m; i++){
int u, v;
cin >> u >> v;
adj[u].push_back(v);
}
for(int i = 1; i <= n; i++){
if(!sccNo[i]) tarjan(i);
}
for(int u = 1; u <= n; u++){
for(auto v : adj[u]){
if(sccNo[u] == sccNo[v]) continue;
otd[sccNo[u]]++;
}
}
for(int i = 1; i <= sccCnt; i++){
if(otd[i] == 0){
if(noOut){
cout << 0 << endl;
return 0;
}
noOut = i;
}
}
cout << sccSz[noOut] << endl;
return 0;
}
|
结论/引理:
在一个DAG中,从所有入度为0的点出发可以到达所有点。
代码
| #include<iostream>
#include<vector>
#include<stack>
using namespace std;
const int N = 105;
int n;
vector<int> adj[N];
stack<int> sta;
int dt, sccCnt;
int pre[N], low[N], sccNo[N];
void tarjan(int u){
sta.push(u);
low[u] = pre[u] = ++dt;
for(auto v : adj[u]){
if(pre[v] == 0){
tarjan(v);
low[u] = min(low[u], low[v]);
}
else if(sccNo[v] == 0){
low[u] = min(low[u], pre[v]);
}
}
if(low[u] == pre[u]){
sccNo[u] = ++sccCnt;
while(sta.top() != u){
sccNo[sta.top()] = sccCnt;
sta.pop();
}
sta.pop();
}
}
bool ind[N], otd[N];
int noIn, noOut;
int main(){
cin >> n;
for(int u = 1; u <= n; u++){
while(true){
int v;
cin >> v;
if(v == 0) break;
adj[u].push_back(v);
}
}
for(int i = 1; i <= n; i++){
if(sccNo[i] == 0) tarjan(i);
}
for(int u = 1; u <= n; u++){
for(auto v : adj[u]){
if(sccNo[u] == sccNo[v]) continue;
ind[sccNo[v]] = 1;
otd[sccNo[u]] = 1;
}
}
for(int i = 1; i <= sccCnt; i++){
if(ind[i] == 0) noIn++;
if(otd[i] == 0) noOut++;
}
if(sccCnt == 1) cout << 1 << endl << 0 << endl;
else cout << noIn << endl << max(noIn, noOut) << endl;
return 0;
}
|