前言
之前每次需要計算強連通分量的時候都用的 \(\text{Kosaraju}\),主要是感覺 \(\rm Tarjan\) 好玄學,我的智商駕馭不了這個玩意兒。
但是,\(\rm Tarjan\) 真的太強大了!隨便做道圖論都有它!於是只有重學一遍,我真的是被逼的。
無向圖
割點
先上代碼吧:
for(int i=1;i<=n;++i)
if(!dfn[i]) tarjan(i,i);
void tarjan(int u,int fa) {
dfn[u]=low[u]=++idx;
int son=0;
for(int i=0;i<e[u].size();++i) {
int v=e[u][i];
if(v==fa) continue;
if(!dfn[v]) {
++son;
tarjan(v,u);
if((u^fa) and low[v]>=dfn[u])
cut[u]=1;
low[u]=min(low[u],low[v]);
}
else low[u]=min(low[u],dfn[v]);
}
if(u==fa and son>1) cut[u]=1;
}
定義 \(\text{dfn}_u\) 是遍歷 \(u\) 的時間戳,\(\text{low}_u\) 是 \(u\) 在 不經過父親 時能到達的時間戳最小的點的時間戳,初始時 \(\text{low}_u=\text{dfn}_u\)。我們先構建出一棵 \(\rm dfs\) 樹,由於邊是雙向的,容易發現邊只有樹邊與返祖邊。這也是循環中的 if-else
判斷。
那么當 \(u\) 通過樹邊連接到 \(v\) 時,如果 \(\text{low}_v\ge \text{dfn}_u\),就說明 \(v\) 子樹中沒有點可以不經過 \(u\) 到達上層,所以 \(u\) 是割點。需要注意的是,每個連通塊的根都滿足這一條件,但顯然並不是所有根都是割點,我們需要額外判斷:記錄 \(son\) 表示根的 不經過根無法連通 的兒子的個數,那么當 \(son>1\),根即為割點。
為什么當 \(u\) 通過返祖邊連接到 \(v\) 時,\(\text{low}_u\) 被 \(\text{dfn}_v\) 更新呢?首先強調一下 else
中還藏着一個 \(v\neq \rm fa\) 的判斷。問題在於,用 \(\text{low}_v\) 更新並不能保證在返回時用樹邊更新 \(\text{low}_x\) 時(假設 \(x\) 是 \(v\) 的某個祖先)的用於更新的值不經過 \(x\) 的父親。
很容易列舉的反例是 \((x,y),(y,z),[z,x]\)(\([]\) 表示返祖邊),如果 \(x\) 在進入 \(y\) 的子樹之前進入另一個子樹,更新了自己的 \(\rm low\) 且比自己的 \(\rm dfn\) 小,那么 \(\text{low}_y\leftarrow \text{low}_z\leftarrow \text{low}_x\),我們就會以為 \(y\) 可以不通過 \(x\) 往上。
點雙連通分量
懂了割點之后,這個還是很好理解的。需要注意根是孤兒的情況,而且一個割點可能被多個點雙連通分量包含。
此時根不一定是割點,但我們仍需要利用它將剩余的點塞進一個點雙連通分量中。另外,while()
循環應到 \(v\) 停止而不是 \(u\),不然會塞進去一些另外的點。
for(int i=1;i<=n;++i)
if(!dfn[i]) tarjan(i,i);
void tarjan(int u,int fa) {
dfn[u]=low[u]=++idx; stk[++tp]=u;
if(u==fa and !head[u]) {
dcc[++Dcc].push_back(u);
return;
}
for(int i=head[u];i;i=e[i].nxt) {
int v=e[i].to;
if(v==fa) continue;
if(!dfn[v]) {
tarjan(v,u);
if(low[v]>=dfn[u]) {
dcc[++Dcc].push_back(u);
while(stk[tp]^v)
dcc[Dcc].push_back(stk[tp--]);
dcc[Dcc].push_back(stk[tp--]);
}
low[u]=min(low[u],low[v]);
}
else low[u]=min(low[u],dfn[v]);
}
}
橋
若 \(u\rightarrow v\) 是一條返祖邊,仍然是用 \(\text{dfn}_v\) 來更新,原因同上。需要注意 重邊 的問題,這可能會使橋變成非橋。
void tarjan(int u,int fa) {
dfn[u]=low[u]=++idx;
bool Vis = false;
for(int i=head[u];i;i=e[i].nxt) {
int v=e[i].to;
if(!dfn[v]) {
tarjan(v,u);
if(low[v]>dfn[u])
then (u,v) is a bridge.
low[u]=min(low[u],low[v]);
}
else if(v==fa and !Vis) Vis = true;
else low[u]=min(low[u],dfn[v]);
}
}
邊雙連通分量
此時不在判斷 low[v]>dfn[u]
的時候求解,會漏算孤兒的情況。
/*
網上很多代碼有 inStack 數組,不太明白用來干嘛...
*/
void tarjan(int u,int fa) {
dfn[u]=low[u]=++idx; stk[++tp]=u;
bool Vis = false; int dot;
for(int i=head[u];i;i=e[i].nxt) {
int v=e[i].to;
if(!dfn[v]) {
tarjan(v,u);
low[u]=min(low[u],low[v]);
}
else if(v==fa and !Vis) Vis = true;
else low[u]=min(low[u],dfn[v]);
}
if(low[u]==dfn[u]) {
++scc;
do {
bl[dot=stk[tp--]]=scc;
} while(dot^u);
}
}
有向圖
強連通分量
此時也在循環外求解 \(\rm scc\),不然會漏算一些情況。
另外,還需要使用 inS[]
數組。只有 \(\text{inS}_v\) 為真時才能更新,考慮滿足這個條件的點一定滿足 "已遍歷過" 且 "不在棧里",它已經被划分到一個強連通分量內了。這時的 \(\text{dfn}_v\) 更小並不是可以走到 \(u\) 的祖先,而是這個強連通分量比 \(u\) 所在的先遍歷到,所以不能用它來更新。
再提一嘴,這里用 \(v\) 更新時用 \(\text{dfn}_v,\text{low}_v\) 更新均可。為啥捏?還是考慮之前證明用的 \(x,y,z\),無論用什么更新,\(\text{dfn}_y\) 也不可能等於 \(\text{low}_v\),這對我們來說就夠了。
void tarjan(int u,int fa) {
dfn[u]=low[u]=++idx; int v;
stk[++tp]=u; inS[u]=1;
for(int i=0;i<n;++i) {
if(~adj[u]>>(v=i)&1)
continue;
if(!dfn[v]) {
tarjan(v,u);
low[u] = min(low[u],low[v]);
}
else if(inS[v])
low[u] = min(low[u],dfn[v]);
// Notice that low[v] is also acceptable.
}
if(low[u]==dfn[u]) {
do {
inS[v=stk[tp--]]=0;
bl[v]=scc;
} while(u^v);
++scc;
}
}