學習Tarjan前提須知
Tarjan是一個能夠求強連通分量的算法。何為強聯通?就是在一個圖中,兩點可以相互到達從而形成的一個環,我們稱這個環為強聯通,其中,在這個圖中所能組成點最多的環,我們稱它為強連通分量,而我們的Tarjan就能求強聯通與強聯通分量 甚至能進行縮點等一系列操作
算法內容
競賽需要用到的點
1、Tarjan求出強聯通后自由度很高,建議不要和強連通分量綁在一起
2、Tarjan較為常見,考慮可以組成一套模型來使用
Tarjan求強聯通分量略講
本人對Tarjan的low算法理解不深,也沒有哪個博客作出具體的解釋,這里就引用wiki了
Tarjan 算法
Robert E. Tarjan (1948~) 美國人。
Tarjan 發明了很多算法結構。光 Tarjan 算法就有很多,比如求各種連通分量的 Tarjan 算法,求 LCA(Lowest Common Ancestor,最近公共祖先)的 Tarjan 算法。並查集、Splay、Toptree 也是 Tarjan 發明的。
我們這里要介紹的是在有向圖中求強連通分量的 Tarjan 算法。
另外,Tarjan 的名字 j
不發音,中文譯為塔揚。
DFS 生成樹
在介紹該算法之前,先來了解 DFS 生成樹 ,我們以下面的有向圖為例:
有向圖的 DFS 生成樹主要有 4 種邊(不一定全部出現):
- 樹邊(tree edge):綠色邊,每次搜索找到一個還沒有訪問過的結點的時候就形成了一條樹邊。
- 反祖邊(back edge):黃色邊,也被叫做回邊,即指向祖先結點的邊。
- 橫叉邊(cross edge):紅色邊,它主要是在搜索的時候遇到了一個已經訪問過的結點,但是這個結點 並不是 當前結點的祖先時形成的。
- 前向邊(forward edge):藍色邊,它是在搜索的時候遇到子樹中的結點的時候形成的。
我們考慮 DFS 生成樹與強連通分量之間的關系。
如果結點 \(u\) 是某個強連通分量在搜索樹中遇到的第一個結點,那么這個強連通分量的其余結點肯定是在搜索樹中以 \(u\) 為根的子樹中。\(u\) 被稱為這個強連通分量的根。
反證法:假設有個結點 \(v\) 在該強連通分量中但是不在以 \(u\) 為根的子樹中,那么 \(v\) 到 \(u\) 的路徑中肯定有一條離開子樹的邊。但是這樣的邊只可能是橫叉邊或者反祖邊,然而這兩條邊都要求指向的結點已經被訪問過了,這就和 \(u\) 是第一個訪問的結點矛盾了。得證。
Tarjan 算法求強連通分量
在 Tarjan 算法中為每個結點 \(u\) 維護了以下幾個變量:
- \(dfn[n]\) :深度優先搜索遍歷時結點 u 被搜索的次序。
- \(low[u]\) :設以 u 為根的子樹為 \(Subtree(u)\) 。\(low[u]\) 定義為以下結點的 \(dfn\) 的最小值: \(Subtree(u)\) 中的結點;從 \(Subtree(u)\) 通過一條不在搜索樹上的邊能到達的結點。
一個結點的子樹內結點的 dfn 都大於該結點的 dfn。
從根開始的一條路徑上的 dfn 嚴格遞增,low 嚴格非降。
按照深度優先搜索算法搜索的次序對圖中所有的結點進行搜索。在搜索過程中,對於結點 和與其相鄰的結點 \(v\) (v 不是 u 的父節點)考慮 3 種情況:
- \(v\) 未被訪問:繼續對 v 進行深度搜索。在回溯過程中,用 \(low[v]\) 更新 \(low[u]\) 。因為存在從 \(u\) 到 \(v\) 的直接路徑,所以 \(v\) 能夠回溯到的已經在棧中的結點,\(u\) 也一定能夠回溯到。
- \(v\) 被訪問過,已經在棧中:即已經被訪問過,根據 \(low\) 值的定義(能夠回溯到的最早的已經在棧中的結點),則用 \(dfn[v]\) 更新 \(low[u]\) 。
- \(v\) 被訪問過,已不在在棧中:說明 \(v\) 已搜索完畢,其所在連通分量已被處理,所以不用對其做操作。
對於一個連通分量圖,我們很容易想到,在該連通圖中有且僅有一個 \(dfn[u] = low[u]\) 。該結點一定是在深度遍歷的過程中,該連通分量中第一個被訪問過的結點,因為它的 DFN 值和 LOW 值最小,不會被該連通分量中的其他結點所影響。
因此,在回溯的過程中,判定 \(dfn[u] = low[u]\) 的條件是否成立,如果成立,則棧中從 \(u\) 后面的結點構成一個 SCC。
實現代碼如下 參考LuoGuP2341強連通分量模板
[此代碼未編譯 可能會有問題 請斟酌參考]
//#define fre yes
#include <cstdio>
#include <cstring>
#include <iostream>
const int N = 50005;
int low[N], dfn[N];
int head[N << 1], to[N << 1], ver[N << 1];
int color[N], Stack[N], de[N];
bool Vis[N];
int tot;
void addedge(int x, int y) {
ver[tot] = y;
to[tot] = head[x];
head[x] = tot++;
}
int num, top, col;
void tarjan(int x) {
dfn[x] = low[x] = ++num;
Stack[++top] = x;
Vis[x] = 1;
for (int i = head[x]; ~i; i = to[i]) {
int v = ver[i];
if(!dfn[v]) {
tarjan(v);
low[x] = std::min(low[x], low[v]);
} else if(Vis[v]) {
low[x] = std::min(low[x], dfn[v]);
}
}
if(dfn[x] == low[x]) {
++col;
color[x] = col;
Vis[x] = 0;
while(Stack[top] != x) {
color[Stack[top]] = col;
Vis[Stack[top--]] = 0;
} top--;
}
}
int main() {
memset(head, -1, sizeof(head));
static int n, m;
scanf("%d %d", &n, &m);
for (int i = 1; i <= m; i++) {
int u, v;
scanf("%d %d", &u, &v);
addedge(u, v);
}
for (int i = 1; i <= n; i++) {
if(!dfn[i]) {
tarjan(i);
}
}
for (int i = 1; i <= n; i++) {
for (int j = head[i]; ~j; j = to[j]) {
int v = ver[j];
if(color[i] != color[v]) {
de[color[i]]++;
}
}
}
int ans = 0, u = 0, k = 0;
for (int i = 1; i <= col; i++) {
if(!de[i]) {
u++;
k = i;
}
}
if(u == 1) {
for (int i = 1; i <= n; i++) {
if(color[i] == k) ans++;
} printf("%d\n", ans);
} else puts("0");
return 0;
}