前言
今天想起來Kosaraju,網上關於這個算法的介紹比較少。(畢竟Tarjan太強了)。但是Tarjan和Kosaraju的複雜度都是的,Kosaraju的常數要大一點。(網上有的博客說kosaraju會卡爆棧,個人感覺不會,退化成鏈的情況Tarjan和Kosaraju都會一搜到底)。
那爲什麼Kosaraju常數大還要學它呢,用Tarjan不好嗎?
因爲它簡單、好理解啊。畢竟Tarjan難理解是出了名的。
正題
一些必要概念
網上介紹各種概念五花八門,不夠深入淺出。首先要理解這幾個概念:
- 前序序列(從一點開始遍歷,結點進入的序列)
- 後序序列(從一點開始遍歷,結點退出的序列)
- 逆後序序列(就是後序序列的逆序,沒什麼高深的意思,
所以百度搜不到) - 圖
- 反圖(將圖的各個邊反過來重新建圖,出邊改入邊)
- 強連通分量SCC(移步百度)
求前序序列和後序序列的代碼(如果上面不理解,看看代碼就懂了)
int n, dcnt, fcnt, c[N], d[N], vis[N], f[N];
vector<int> G[N];
void dfs(int x) {
d[x] = ++dcnt;
vis[x] = 1;
for (auto y : G[x]) {
if (!vis[y]) dfs(y);
}
v[x].n = ++fcnt;
}
void solve() {
dcnt = fcnt = 0;
memset(vis, 0, sizeof(vis));
for (int i = 1; i <= n; i++) {
if (!vis[i]) dfs(i);
}
}
Kosaraju如和實現
兩遍DFS:
-
第一遍,求出圖G的逆後序序列。
-
第二遍,根據逆後序序列,在反圖上進行DFS,每次能dfs點就在一個強連通分量裏。
代碼:
int n, c[N], dfn[N], vis[N], dcnt, scnt;
vector<int> G1[N], G2[N]; // G1 原圖,G2 反向圖
void dfs1(int x) { // 求後序序列
vis[x] = 1;
for (auto y : G1[x]) {
if (!vis[y]) dfs1(y);
}
dfn[++dcnt] = x;
}
void dfs2(int x) {
c[x] = scnt;
for (auto y : G2[x]) {
if (!c[y]) dfs2(y);
}
}
void kosaraju() {
dcnt = scnt = 0;
memset(c, 0, sizeof(c));
memset(vis, 0, sizeof(vis));
for (int i = 1; i <= n; i++) {
if (!vis[i]) dfs1(i);
}
// 反過來遍歷dfn就是逆後序序列
for (int i = n; i >= 1; i--) {
if (!c[dfn[i]]) ++scnt, dfs2(dfs[i]);
}
}
Why?如何理解
詳細的數學證明請參考《算法導論》,這裏給出如何一種正確理解的方法。
首先要知道:
- 原圖和反圖具有相同的SCC(強連通分量)。
那爲什麼要求後序序列或者逆後序序列呢?
實際上是在求一個拓撲排序,但是帶環圖沒有拓撲排序的概念,這個逆後序序列就差不多是原圖縮點後的拓撲排序序列。
如圖所示,從1號節點開始逆後序序列爲:8、1、3、2、7、4、5、6
縮點後就是:
此圖的拓撲排序爲:S3(8)、S2(1、3、2)、S1(7、4、5、6)
然後如果在反圖上按照逆拓撲序列遍歷的話每次只會遍歷到一個SCC。這樣,這個算法就可以正常求出所有強連通分量了
問題
爲什麼要在反圖上做逆後序序列?在原圖上做後序序列不可以嗎?
是這樣的,上述給的例子,在原圖上做後序序列是完全可以的。但只要稍加改動,原圖的逆序序列有很多種(並不唯一)。比如:
- 逆後序序列1:8、1、3、2、7、4、5、6
- 逆後序序列2:8、1、3、7、4、5、6、2
序列1、2都是合法的逆序序列。上面介紹用的逆後序序列1。
他們的後序序列:
-
後序序列1:6、5、4、7、2、3、1、8
-
後序序列2:2、6、5、4、7、3、1、8
這裏大家需要手動模擬一下(很簡單),如果在原圖上採用後序序列2,會得到錯誤的答案。但是在反圖上採用逆後序序列2,答案仍舊正確。
因此我們只能在反圖上做逆後序序列。