概念
- 連通分量:針對於無向圖而言,圖內任意兩點
u,v
可以相互到達。 - 強連通分量:針對於有向圖而言,圖內任意兩點
u,v
可以相互到達。 - 弱連通分量:針對於有向圖而言,將圖看作無向圖,可以滿足連通分量的條件。
- 半連通分量:針對於有向圖而言,圖內任意兩點
u,v
,u→v,v→u
至少滿足一個。
求強連通分量往往只是一種手段,常常用於縮點操作,講強連通分量看作一個點,然后將圖變為一個拓撲圖(有向無環圖DAG)。所以實際上很多強連通分量的題同時也是拓撲序的題, 下面提到的例題也全部和拓撲序有關. 如果有關於拓撲序的知識點不會可以轉移到本人的另一篇博客算法專題——拓撲序.
求強連通分量
可以使用dfs
的方法求解強連通分量,即tarjan
算法。
在了解tarjan
算法是什么東西之前,需要先知道幾個前置知識,有向圖中邊的分類。如下圖所示,總共有四類邊。
- 樹枝邊:父節點指向子節點的邊。
- 前向邊:祖先節點指向子孫節點的邊。
- 后向邊:子孫節點指向祖先節點的邊。
- 橫叉邊:當前節點指向右邊已遍歷的其他分支的節點的邊。
可以發現一個強連通分量必然是由無數個環組詞的圖,最簡單的強連通分量是一個環,而復雜的強連通分量則是無數個環通過組合拼接而成的“環”,借助強連通分量可以互相到達的特點,我們可以先給一個節點標上記號,通過強連通分量環中有向邊的傳遞,僅讓同屬於一個強連通分量的節點都具有某種標記,從而可以得到強連通分量。
而為了讓強連通分量都具有某種標記,可以通過dfs
的方法實現,給每一個節點設定兩個屬性,一個是在dfs
遍歷中節點被訪問的序號dfn[i]
,另一個就是節點通過有向邊,可以訪問到的最小dfn
記為low[i]
,即記錄節點所屬的強連通分量中節點dfn
最小的值。這里的low
數組就是我們上面提到的記號。
體現在四種類型的邊中,存在環的情況僅兩種情況。其一,存在后向邊指向祖先節點;其二,先走到橫叉邊,橫叉邊再走到祖先節點。
tarjan
算法基於一個棧和兩個標記數組實現。除了以上的標記方法之外,還要注意對屬於一個強連通分量的節點進行標記,這里通過一個棧實現。由於強連通分量是一塊塊隨着dfs
順序出現的,所以我們可以使用一個棧存儲途中遍歷到的所有點,當回溯遇到dfn[i] == low[i]
的情況(強連通分量中dfn
最小的節點),就將棧內元素的整個強連通分量彈出。
代碼:
void Tarjan(in u) {
dfn[u] = low[u] = +timestamp;
stk[++top] = u, in_stk[u] = true;
for (int i = h[u]; ~i; i = ne[u]) {
int v = e[i];
if (!dfn[v]) {
tarjan(v);
low[u] = min(low[u], low[v]);
}
else if (in_stk[v])
low[u] = min(low[u], dfn[v]);
}
if (dfn[u] == low[u]) {
scc_cnt++;
int y;
do {
y = stk[top--];
in_stk[y] = false;
id[y] = scc_cnt;
size[scc_cnt]++;
} while (y !- u); //while將強連通分量彈出
}
}
例題
Popular Cows
題面:
分析:
將題意轉換之后可以得到, 要求解有多少個點, 滿足圖中除節點本身之外的所有點都可以到達該節點. 在一個強連通分量里面, 各個節點之間的點都是可以相互到達的, 所以可以將強連通分量進行縮點, 進而變為DAG, 結合拓撲序的知識點可以發現在DAG中, 要擁有一個節點使得所有點均可以到達該點點, 需要滿足該點的出度為0, 且其他節點的出度均不為0.
核心代碼:
for (int i = 1; i <= n; i++) //縮點
if (!dfn[i]) Tarjan(i);
for (int u = 1; u <= n; u++) //統計出度
for (int j = h[u]; ~j; j = ne[j])
if (id[u] != id[e[j]])
dout[id[u]]++;
int res = 0;
int flag = false; //標記有多少個出度為0的點
for (int i = 1; i <= scc_cnt; i++)
if (!dout[i]) {
flag = !flag;
res = size[i]; //記錄該節點有多少個子節點
if (!flag) { //出現第二個出度為0的點
res = 0;
break;
}
}
cout << res << endl;
Network of Schools
題面:
分析:
題意可以理解為, 節點u向節點v傳軟件表示節點u可以到達節點v.
一個強連通分量內, 任意兩點都可以到達, 進行縮點, 得到DAG. 結合拓撲序的知識點, 可以得到第一問的答案為入度為零的點的個數, 第二問的答案為max(入度為0的點的個數, 出度為0的點的個數)
. 核心代碼見下.
for (int u = 1; u <= n; u++) {
for (int i = h[u]; ~i; i = ne[i]) {
int v = e[i];
if (id[u] != id[v]) {
din[id[v]] ++;
dout[id[u]] ++;
}
}
}
int a = 0, b = 0;
for (int i = 1; i <= scc_cnt; i++) {
if (!din[i]) a++;
if (!dout[i]) b++;
}
cout << a << endl;
if (scc_cnt == 1) cout << 0 << endl; //特判. 注意在推結論的時候最后需要考慮邊界情況, 否則可能會wa到哭
else cout << max(a, b) << endl;
最大半連通子圖
題面:
分析:
還是先縮點, 可以得到一個DAG, 結合拓撲序的知識點, 可以得到一個強連通分量內的節點可以相互到達, 不用考慮, 而在DAG內, 可以發現半連通分量實際上就是一條鏈. 所以可以使用拓撲DP, 得到規模最大的一條鏈, 並記錄其貢獻, 全部代碼以及具體注釋見下.
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<unordered_set>
#define ll long long
using namespace std;
//找一個最長的拓撲鏈
const int MAXN = 1e5 + 10, MAXM = 2e6 + 10;
int n, m, p;
int h[MAXN], hs[MAXN], e[MAXM], ne[MAXM], idx;
int dfn[MAXN], low[MAXN], timestamp;
int stk[MAXN], top;
bool in_stk[MAXN];
int scc_cnt, id[MAXN], size[MAXN];
int dp[MAXN], g[MAXN];
void AddEdge(int h[], int a, int b) {
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
void Tarjan(int u) { //求強連通分量, 要記錄每一個節點的歸屬以及每一個連通分量的大小
dfn[u] = low[u] = ++timestamp;
stk[++top] = u; in_stk[u] = true;
for (int i = h[u]; ~i; i = ne[i]) {
int v = e[i];
if (!dfn[v]) {
Tarjan(v);
low[u] = min(low[u], low[v]);
}
else if (in_stk[v]) low[u] = min(low[u], dfn[v]);
}
if (dfn[u] == low[u]) {
int y;
scc_cnt++;
do {
y = stk[top--];
in_stk[y] = false;
id[y] = scc_cnt;
size[scc_cnt]++;
} while (y != u);
}
}
int main() {
scanf("%d%d%d", &n, &m, &p);
memset(h, -1, sizeof h);
memset(hs, -1, sizeof hs);
int a, b;
for (int i = 1; i <= m; i++) {
scanf("%d%d", &a, &b);
AddEdge(h, a, b);
}
for (int i = 1; i <= n; i++)
if (!dfn[i]) Tarjan(i);
unordered_set<ll> S; //防止重復建圖, 使用一個哈希
for (int u = 1; u <= n; u++) { //建圖
for (int i = h[u]; ~i; i = ne[i]) {
int v = e[i];
a = id[u], b = id[v];
ll tmp = a * 1000000ll + b; //哈希函數使用a向前移動再與b並在一起
if (a != b && !S.count(tmp)) {
AddEdge(hs, a, b);
S.insert(tmp);
}
}
}
for (int u = scc_cnt; u; u--) { //利用拓撲進行dp
if (!dp[u]) { //第一個點要特殊處理
dp[u] = size[u];
g[u] = 1;
}
for (int i = hs[u]; ~i; i = ne[i]) { //往后更新, 同時更新dp和g數組
int v = e[i];
if (dp[v] < dp[u] + size[v]) {
dp[v] = dp[u] + size[v];
g[v] = g[u];
}
else if (dp[v] == dp[u] + size[v])
g[v] = (g[v] + g[u]) % p;
}
}
int maxans = 0, sum = 0; //再遍歷一遍, 得到最大的dp以及對應的g數組
for (int i = 1; i <= scc_cnt; i++) {
if (dp[i] > maxans) {
maxans = dp[i];
sum = g[i];
}
else if (dp[i] == maxans) sum = (sum + g[i]) % p;
}
printf("%d\n%d\n", maxans, sum);
return 0;
}