tarjan 演算法與圖的連通性
前言與預備知識
發現我根本不會 tarjan,又發現《演算法競賽進階指南》上正好有相關講解,於是回來補 tarjan 這個 NOIP 演算法。 (順便頹一會兒水題)
首先我們要知道 搜尋樹 的相關內容(注意區分搜尋樹和原圖):
定義 \(dfn[cur]\) 為 \(cur\) 節點的時間戳。
\(low[cur]\) 為 \(cur\) 節點的追溯值。
其中 \(low[cur]\) = \(min\){搜尋樹中 \(cur\) 的子樹的節點的時間戳, 子樹中通過一條邊,能夠到達的節點的時間戳}
警告:分清 \(dfn\) 和 \(low\)!
無向圖相關問題
橋 與 邊雙連通分量
橋
無向圖中,如果割掉一條邊,可以使整個無向圖成為兩個連通塊,那麼這條邊成為割邊
判定法則:
\[low[to] > dfn[cur] \]
顯然,橋一定是搜尋樹上的邊, 簡單環中的邊一定不是橋(P2607 [ZJOI2008]騎士 中找環的方法之一)。
注意:不要用 \(cur\) 的 \(low/dfn\) 來更新 \(fa\) 的 \(low\)。但是可能會遇到重邊等問題,所以記錄入邊編號 \(ine\),防止遍歷 \(ine\)!
求法:
//(initial)ecnt = 1 void tarjan(int cur, int ine) { dfn[cur] = low[cur] = ++dtot; for (register int i = head[cur]; i; i = e[i].nxt) { int to = e[i].to; if (!dfn[to]) { tarjan(to, i); low[cur] = min(low[cur], low[to]); if (low[to] > dfn[cur]) iscut[i] = iscur[i ^ 1] = true; } else if (i != ine ^ 1) low[cur] = min(low[cur], dfn[to]); } }
邊雙連通分量(e-DCC)
不存在割邊的無向連通圖為 邊雙連通圖。極大邊雙連通子圖為 邊雙連通分量。
一張無向連通圖是邊雙連通圖,當且僅當對於圖中每條邊,都在至少一個簡單環上。
- 求法:
將橋刪去後,整個圖就成了一個個邊雙連通分量。
可以對圖進行縮點。點內無橋,點間為橋。這樣的話,縮點後沒有任何環,是無向圖森林。
void tarjan(int cur, int ine); bool vis[N]; int siz[N]; int col[N], ctot; void Dfs(int cur) { vis[cur] = true; col[cur] = ctot; siz[ctot]++; for (register int i = head[cur]; i; i = e[i].nxt) { int to = e[i].to; if (vis[to] || iscut[i]) continue; Dfs(to); } } vector<int> eg[N]; inline void adeg(int u, int v) { eg[u].push_back(v); eg[v].push_back(u); } //in main() for (register int i = 1; i <= n; ++i) { if (!dfn[i]) tarjan(i, 0); } memset(vis, 0, sizeof(vis)); for (register int i = 1; i <= n; ++i) { if (!vis[i]) ctot++, Dfs(i); } for (register int i = 2; i <= ecnt; i += 2) { if (iscut[i]) { adeg(col[e[i].to], col[e[i ^ 1].to]); } }
- 典型應用:無向圖的必經邊
必經邊 = 割掉後點對不連通 = 點對間的割邊(橋)
邊雙縮點+樹剖,查詢點對距離即可。
- 例題:【GDOI2015】水題
題意:
n 個點,m 條邊, q 次詢問,每次問 \(i\) 號邊刪去後會有多少點對互不可達。
n <= 1e5, m <= 1e6, q <= 8e5
發現不刪也會有一堆點對互不可達(原本不一定聯通)。如果刪去的是割邊,那麼會增加其兩端連通塊的大小之積這麼多的點對。
因此首先並查集搞出“初始答案”。然後 \(tarjan\) 求割邊。然後將割邊刪去,進行縮點。此時縮好以後的圖是一棵森林。因此我們可以對每棵樹類似鏈剖的第一個dfs一樣地搞出 \(dep\) 和 \(siz\)。沒了。
注意區分“節點”與“節點”之間的關係。即區分:
森林--樹(連通塊)--樹上的點(邊雙連通分量)--原圖的點
割點 與 點雙連通分量
- 割點
刪去點及其所有連邊後,原無向圖分裂成為多個連通塊,則這一點為 割點。
判定法則:
\[low[to] >= dfn[cur] \]
具體來說,對於(搜尋樹的)非根節點 \(cur\),如果存在 \(low[to] >= dfn[cur]\),那麼 \(cur\) 為割點;對於根節點來說,如果有 至少兩個 \(to\) 符合條件,那麼根節點也為割點。
由於是 \(>=\),因此就算拿 \(fa\) 的 \(dfn\) 來更新點的 \(low\)(顯然不可能用 \(fa\) 的 \(low\) 來更新),也無法使該點跳出包圍圈,不會將其 \(fa\) 誤判為非割點。
求法:
void tarjan(int cur) {
dfn[cur] = low[cur] = ++dcnt;
int cnt = 0;
for (register int i = head[cur]; i; i = e[i].nxt) {
int to = e[i].to;
if (!dfn[to]) {
tarjan(to);
low[cur] = min(low[cur], low[to]);
if (low[to] >= dfn[cur]) {
cnt++;
if (!iscut[cur] && (cur != rt || cnt > 1))
cut[++cuttot] = cur, iscut[cur] = true;
}
} else {
low[cur] = min(low[cur], dfn[to]);
}
}
}
題意 : 給定一張無向聯通圖,求每個點被封鎖(刪去與該點相連的所有邊)之後有多少個有序點對(x,y)(x!=y,1<=x,y<=n)滿足x無法到達y
注意:沒有刪去那個點(不過都是細節了)
如果刪去的不是割點,那麼圖仍然是聯通圖(除了單拎出來一個點),直接特判即可。
如果刪去的是割點,那麼圖將會四分五裂。準確地說,如果對於這個點 \(cur\) 來說有 \(T\) 個 \(to\) 符合 \(low[to] >= dfn[cur]\) 的條件,那麼這張圖最多能分成 \(T + 2\) 個連通塊,包括那麼多 \(to\) 的子樹 + \(cur\)節點 + 除此以外的所有點(這個可能沒有)。
然後跑 \(tarjan\) 的時候記錄一下 \(siz\)(搜尋樹的子樹大小)。同時維護一下所有符合條件的子樹大小之和(方便統計最後一部分對答案的貢獻)。然後直接算每個點的答案就行了。
- 點雙連通分量(v-DCC)
不存在割點的無向連通圖為 點雙連通圖。極大點雙連通子圖為 點雙連通分量。
一張圖是點雙連通圖,當且僅當圖的頂點數不超過2,或者圖的任意兩個點都在同一個簡單環(圓圈)中。
注意:一個割點可能同時屬於多個 v-DCC!(非割點只屬於一個)
- 求法
維護一個棧。無論是否是根,在遇到 \(low[to] >= dfn[cur]\) 時彈棧一直彈到 \(to\),然後彈出的所有點再加上 \(cur\) 即為一個點雙連通分量。
比較麻煩的時縮點。有了一個個 v-DCC 後,就可以進行縮點。但是由於割點可能同時屬於多個 v-DCC,因此我們要將 割點複製一份作為中轉節點。令人欣慰的時,縮點過後原圖將成為一棵樹(或森林)。如下圖:
void tarjan(int cur) {
stk[++stop] = cur; dfn[cur] = low[cur] = ++dcnt;
int cnt = 0;
for (register int i = head[cur]; i; i = e[i].nxt) {
int to = e[i].to;
if (!dfn[to]) {
tarjan(to);
low[cur] = min(low[cur], low[to]);
if (low[to] >= dfn[cur]) {
cnt++;
if (cur != rt || cnt > 1) iscut[cur] = true;
int tmp; dcc_tot++;
do {
tmp = stk[stop--];
dcc[dcc_tot].push_back(tmp);
} while(tmp != to);
dcc[dcc_tot].push_back(cur);
}
} else {
low[cur] = min(low[cur], dfn[to]);
}
}
}
inline void rebuild() {
int ntot = n;
for (register int i = 1; i <= n; ++i) {
if (iscut[i]) newid[i] = ++ntot;
}
for (register int i = 1; i <= dcc_tot; ++i) {
for (register int j = 0; j < (int)dcc[i].size(); ++j) {
int cur = dcc[i][j];
if (iscut[cur]) {
aded(nwid[cur], i); aded(i, nwid[cur]);
} else {
col[cur] = i;
}
}
}
}
- 典型應用:無向圖必經點
例題:P4320 道路相遇
必經點 = 刪去這個點後點對不連通 = 點雙縮點後的樹上路徑的割點樹
需要完整地建出縮點後的樹。為了防止邊混淆,在建立縮點後的樹之前先將原圖的邊清空。
注意對於路徑端點(即點對)需要特判。
(題解裡面好多說圓方樹的,不知道是不是和tarjan做法有關)
\(Code:\)my record
習題:UVA1464 Traffic Real Time Query System
題意:求無向圖中不屬於任何奇環的節點數量。其中1個點不算環。
首先我們最好把一個個環(不一定是簡單環)都提出來。這個要用點雙連通分量。因為邊雙連通分量不好搞定 \(∞\) 形狀的情況。我們要求每個“塊”內的任意兩點都屬於至少同一個環。(畢竟是點在環上的問題,而不是邊在環上的問題)
可以證明,對於每一個塊(v-DCC),如果包含有至少一個奇環,那麼塊內的所有點都在至少一個奇環上。(找到奇環後,由於要求任意兩個點都在至少同一個環上,因此奇環外的點一定會與奇環上的點以環的形式連線,出現環套環的現象。因此那一個環有兩種形態,必定是一種奇環一種偶環)。
於是黑白染色即可。由於割點在不同的DCC上,因此每次染色前需要重置“顏色”。
小於等於兩個點的DCC恰好也符合,正好不用特判。
程式碼:my record
- 類似的題:UVA1464 Traffic Real Time Query System(這道題建議閱讀英文題面)
提示:對於無向圖邊對的必經點數,為四對點的必經點數的最大值。通過找規律可以得到。嚴謹證明應該也不難,就在縮點後的數上討論各種(兩種)情況即可。
程式碼:my record
- 習題
P2860 [USACO06JAN]Redundant Paths G
有向圖相關問題
tarjan 演算法、強連通分量與縮點
有向圖的 tarjan 已經很熟悉了。
注意1:如果 \(to\) 不在棧中,就不要用它更新 \(cur\) 的 \(low\) 了。
注意2:將 \(tmp\) 彈棧時,要 \(vis[tmp] = false\) !
\(Code:\)
void tarjan(int cur) {
dfn[cur] = low[cur] = ++dcnt;
stk[++stop] = cur; vis[cur] = true;
for (register int i = head[cur]; i; i = e[i].nxt) {
int to = e[i].to;
if (!dfn[to]) {
tarjan(to);
low[cur] = min(low[cur], low[to]);
} else if (vis[to]) {
low[cur] = min(low[cur], low[to]);
}
}
if (low[cur] == dfn[cur]) {
int tmp; ctot++;
do {
tmp = stk[stop--];
col[tmp] = ctot;
vis[tmp] = false;
} while (tmp != cur);
}
}
相關題目:P3387 【模板】縮點,P2812 校園網路【[USACO]Network of Schools加強版】,P2515 [HAOI2010]軟體安裝
有向圖的必經邊與必經點
似乎和支配樹有關?這裡只有DAG的必經邊和必經點。
從 \(S\) 出發沿正向邊(即原圖的邊)拓撲dp求出 \(fs[cur]\) 表示 \(S\) 到 \(cur\) 的方案數;從 \(T\) 出發沿反向邊(即原圖的反向邊)拓撲dp求出 \(ft[cur]\) 表示 \(cur\) 到 \(T\) 的方案數。這樣, \(fs[T]\) 即為總方案數。
如果有一條邊\((u, v)\),滿足:\(fs[u] * ft[v] == fs[T]\),即為必經點;
如果有一個點 \(cur\),滿足:\(fs[cur] * ft[cur] == fs[T]\),即為必經點。
由於方案數可能很大,需要進行Hash!
注意:
-
一般圖的連通性的題目要對DCC/SCC為一的情況進行特判。
-
求點雙和求SCC的寫法不太一樣,求點雙是
do { } while (tmp != to);
一直到把 \(to\) 彈出去為止(最好寫成 do-while 形式,否則容易忘記彈 \(to\),SCC同理);而求SCC則是do { } while (tmp != cur);
要把自己彈出去。 -
點雙可以用來水過很多圓方樹的題,比如道路相遇,以及戰略遊戲。
附:
tarjan 求割邊及縮點(除錯用)
//(initial)ecnt = 1
void tarjan(int cur, int ine) {
dfn[cur] = low[cur] = ++dtot;
for (register int i = head[cur]; i; i = e[i].nxt) {
int to = e[i].to;
if (!dfn[to]) {
tarjan(to, i);
low[cur] = min(low[cur], low[to]);
if (low[to] > dfn[cur])
iscut[i] = iscur[i ^ 1] = true;
} else if (i != ine ^ 1)
low[cur] = min(low[cur], dfn[to]);
}
}
bool vis[N];
int siz[N];
int col[N], ctot;
void Dfs(int cur) {
vis[cur] = true;
col[cur] = ctot;
siz[ctot]++;
for (register int i = head[cur]; i; i = e[i].nxt) {
int to = e[i].to;
if (vis[to] || iscut[i]) continue;
Dfs(to);
}
}
vector<int> eg[N];
inline void adeg(int u, int v) {
eg[u].push_back(v); eg[v].push_back(u);
}
//in main()
for (register int i = 1; i <= n; ++i) {
if (!dfn[i]) tarjan(i, 0);
}
memset(vis, 0, sizeof(vis));
for (register int i = 1; i <= n; ++i) {
if (!vis[i]) ctot++, Dfs(i);
}
for (register int i = 2; i <= ecnt; i += 2) {
if (iscut[i]) {
adeg(col[e[i].to], col[e[i ^ 1].to]);
}
}
tarjan 求割點及縮點(除錯用)
void tarjan(int cur) {
stk[++stop] = cur; dfn[cur] = low[cur] = ++dcnt;
int cnt = 0;
for (register int i = head[cur]; i; i = e[i].nxt) {
int to = e[i].to;
if (!dfn[to]) {
tarjan(to);
low[cur] = min(low[cur], low[to]);
if (low[to] >= dfn[cur]) {
cnt++;
if (cur != rt || cnt > 1) iscut[cur] = true;
int tmp; dcc_tot++;
do {
tmp = stk[stop--];
dcc[dcc_tot].push_back(tmp);
} while(tmp != to);
dcc[dcc_tot].push_back(cur);
}
} else {
low[cur] = min(low[cur], dfn[to]);
}
}
}
inline void rebuild() {
int ntot = n;
for (register int i = 1; i <= n; ++i) {
if (iscut[i]) newid[i] = ++ntot;
}
for (register int i = 1; i <= dcc_tot; ++i) {
for (register int j = 0; j < (int)dcc[i].size(); ++j) {
int cur = dcc[i][j];
if (iscut[cur]) {
aded(nwid[cur], i); aded(i, nwid[cur]);
} else {
col[cur] = i;
}
}
}
}
tarjan 求強連通分量(除錯用)
void tarjan(int cur) {
dfn[cur] = low[cur] = ++dcnt;
stk[++stop] = cur; vis[cur] = true;
for (register int i = head[cur]; i; i = e[i].nxt) {
int to = e[i].to;
if (!dfn[to]) {
tarjan(to);
low[cur] = min(low[cur], low[to]);
} else if (vis[to]) {
low[cur] = min(low[cur], low[to]);
}
}
if (low[cur] == dfn[cur]) {
int tmp; ctot++;
do {
tmp = stk[stop--];
col[tmp] = ctot;
vis[tmp] = false;
} while (tmp != cur);
}
}