前言
網上現存\(60\%\)的文章都有明顯的誤區,本文章經過多次修改,能保證正確性
-
本文涉及強連通分量、弱連通分量、割點、割邊、邊雙、點雙,屬於基本圖論范疇
-
在有着直接關聯的基礎上又有所不同,本文基於把抽象的數組轉換為在圖上的意義,旨在讓初學者能更輕松地理解並區分差別
-
為避免各個板子的差別過大,在正確的基礎上盡量保證代碼的相似性
如果您之前學過,可能與您的定義有所不同,故請在看完每個算法下面的代碼后再進行文字閱讀
- 文字中某個詞語后出現帶圓框的數字,如①②,這些詞語將會在文字下方有詳細的注釋,方便閱讀
前置
我們簡略地定義\(dfs\)樹為遍歷路程中路徑所組成的一棵樹,注意下面說的兒子、葉子節點、子樹邊界\((\)與子樹直接相連的外部節點\()\)等用法都從此基礎上得出
如下圖及\(dfs\)樹,\(3,7\)為\(1\)的兒子,葉子節點為\(2,5,6,8\),\(7\)的子樹邊界為\(\{1\}(1\)與\(7\)和\(8\)直接相連\()\),如果新加一條邊\((3,4)\),則\(7\)的子樹邊界為\(\{1,3\}\)
有向圖
強連通分量
定義:有向圖中某個點集中的點互相能到達的分量為強連通分量
為方便理解我們采取歸納法:找到完整強連通分量后立即染色
- 定義\(dfn_u\):表示\(dfs\)中\(u\)的時間戳;初始化為第幾個被遍歷到的點。
- 定義\(low_u\):表示\(u\)能到達且在\(u\)子樹邊界的未染色的最小時間戳①\((\)設代表該最小時間戳點的點為\(x\),可證明\(x\)一定能與\(u\)組成強連通分量②\()\);初始化為\(dfn_u\)。
①:顯然代表該最小時間戳的不為\(u\)的子樹\((\)除\(u)\),因為子樹內的時間戳\(u\)已經為最小的了。故\(u\)的子樹並不影響\(low_u\),真正影響的是\(u\)的子樹外,與\(u\)子樹有接觸,且未染色的。
②:\((\)下圖為例\()x\)位於\(f\)的左子樹,\(x\)所在完整強連通內所有節點不止在左子樹\((\)否則就染色了\()\),\(x\)至少能與\(f\)組成強連通分量。故\(x\)一定能與\(u\)組成強連通分量:\(f\rightarrow u\rightarrow x\rightarrow f\)。
具體做法:在\(u\)的子樹遍歷完后,\(low_u=dfn_u\)則把棧頂到該點的區間染色\((\)與子樹外單向聯通,那\(u\)的子樹未處理部分與\(u\)組成強連通分量\()\),否則要等回到某個祖先后染色才能分量的完整
code
也可更換第\(7\)行代碼為:
else if(visit[v]) low[u]=std::min(low[u],low[v]);
//此時定義low:能與u組成強連通分量(未染色)的最小時間戳
void Tarjan(LL u){
dfn[u]=low[u]=++tim; sta[++top]=u; visit[u]=true;
for(LL i=head[u];i;i=dis[i].nxt){
LL v(dis[i].to);
if(!dfn[v]){
Tarjan(v); low[u]=std::min(low[u],low[v]);
}else if(visit[v]) low[u]=std::min(low[u],dfn[v]);
}
if(low[u]==dfn[u]){
LL now; ++nod;
do{
now=sta[top--]; col[now]=nod; visit[now]=false;③
}while(now!=u);
}
}
③:\(visit\)清空:強連通分量是建立在有向圖上的,\(u\rightarrow v,v\nrightarrow u\)時,如果之前先遍歷過\(v\),則已經把\(visit\)清空了,此時\(u\)不受\(v\)的任何影響
弱連通分量
定義:同一弱連通分量里的任意兩個點\(x,y\),保證至少一方能到達另一方
想象一下某個弱連通分量進行強連通縮點后的樣子?能兩兩到達的肯定存在於同一個大點中了,剩下的肯定是單向聯通,故一定是一條單直鏈
性質:某一點可能屬於多個弱連通分量,顯然,屬於強連通分量的兩點一定屬於同一弱連通分量
做法:在強連通縮點后的\(DAG\)圖中,每一條鏈就是一個弱連通分量
無向圖
為了割點與橋的統一計算,在無向圖中我們不管父親\((\)類似樹的遍歷,遇到\(f\)則\(continue)\),稍后會說明原因
且重新定義:
- 定義\(low_u\):\(u\)的子樹與子樹外接觸的最小時間戳\((\)除\((u,f)\)的影響,因為在遍歷\(u\)時遇到\(f\)會跳過\()\)
如下圖,藍邊為\(dfs\)樹,標號為時間戳,\(6\)的子樹為\(\{6,7,8,9\}\),子樹與子樹外的接觸為\((6,3),(7,5),(9,2)\),故\(low_6=2\)
割點
定義:無向圖中,將該點從原圖中拿掉后,連通分量數量增加
想象一下割點在圖上的樣子:一個點至少夾在兩個互不接觸 (( 不考慮該點的連接作用 )) 聯通塊之間
為了便於理解,先想一下暴力做法④:特判每一個點,如果該點至少有兩個兒子則說明為割點\((\)這些兒子所屬的子樹互不接觸,否則僅需遍歷一次,而該點位於子樹之間,顯然割掉后連通塊個數=兒子個數\()\)。時間復雜度\(O(nm)\)
④:因為判斷兒子得把整個子樹全跑一遍。而每一次判斷的兒子個數僅對根有效。因為根肯定得把一個子樹的點遍歷完才能回溯;其他的點由於順序關系,入度也會成為一個兒子\((\)對於無根樹\()\),實際上入度上方可能與兒子有接觸,故不能一次性判斷。比如下面的這幅圖,從\(1\)開始遍歷則\(3\)有兩個兒子;但從\(3(root)\)開始遍歷:\(3\longrightarrow 2\longrightarrow 1\longrightarrow 12\longrightarrow 13\longrightarrow9\longrightarrow8\longrightarrow6\longrightarrow7\longrightarrow5\longrightarrow4\longrightarrow10\longrightarrow11\),最后得到的是\(3\)只有一個兒子,所以\(3\)不為割點
轉換成條件:對於\((u,v),fa_v=u\),在割掉\(u\)后,\(v\)的子樹與外界無任何接觸。也就是\(v\)的子樹僅與外界的\(u\)接觸,則當割掉\(u\)后,多生成了一個聯通分量。
抽象成代碼:\(u\in Articulation Point \longrightarrow low[v]>=dfn[u]\)
細節:如果我們首先遍歷\(u(root)\),則無論怎樣\(low_v\)都會\(≥dfn_u(dfn_u=1)\),則根據定義是把\(u\)判斷為割點,其實不然⑤。
⑤:有時我們發現根不會為割點,這是為什么呢?因為\(u\)的子樹就是所有點,故沒有外界,也就是說特判一定滿足,故該特判對其無效。
所以需要判斷的是\(u\)是否有至少兩個兒子\((\)原理就是上面的暴力做法\()\),否則就為無根樹上的葉子節點了\((\)也就是邊界\()\)
code
void Tarjan(LL u,LL mr,LL f){
LL rc(0);
dfn[u]=low[u]=++cnt;
for(LL i=head[u];i;i=dis[i].nxt){
LL v(dis[i].to);
if(v==f) continue;
if(!dfn[v]){
Tarjan(v,mr,u);
low[u]=min(low[u],low[v]);
if(low[v]>=dfn[u]&&u!=mr)
cut[u]=true;
if(u==mr)
rc++;
}else low[u]=min(low[u],dfn[v]);
}
if(u==mr&&rc>=2)
cut[mr]=true;
}
橋
定義:又稱為割邊,將該邊從原圖中拿掉后,連通分量數量增加
想象一下橋在圖上的樣子:一條邊被兩個不接觸\((\)不考慮這條邊的連接作用\()\)的聯通塊夾在中間。
如下方,兩個"\(+\)點"間的為橋
為了便於理解,先想一下暴力做法:枚舉每一條邊\((x,y)\)從\(x\)節點出發不經過該邊遍歷一次,如果不能到達\(y\)則該邊為橋。時間復雜度\(O(m^2)\)
轉換成條件:對於橋\((u,f),fa_u=f\)的,割掉\((u,f)\)后,\(u\)的子樹外界無任何接觸。也就是除\((u,f)\)外,\(u\)的子樹的邊僅局限在內部,相當於u的子樹被一根線掛在\(f\)節點上。
抽象成代碼:\((u,v)\in Bridge\longrightarrow low[v]>dfn[u]\)
void Tarjan(LL u,LL f){
dfn[u]=low[u]=++cnt;
for(LL i=head[u];i;i=dis[i].nxt){
LL v(dis[i].to);
if(v==f) continue;
if(!dfn[v]){
Tarjan(v,u);
low[u]=min(low[u],low[v]);
if(low[v]>dfn[u]){
e[++tot][0]=u; e[tot][1]=v;
}
}else low[u]=min(low[u],dfn[v]);
}
}
其實割點是可以考慮父親的影響,而橋絕對不能
因為\(f\)為割點僅需要考慮\(u\)的子樹最多遍歷到\(f\),也就是說\((u,f)\)這條邊對判斷起不到任何影響
而橋\((f,u)\)需要:除開這條邊,\(u\)的子樹最多遍歷到\(u\)。如果考慮父親的影響,\(low_u\)一定會受\(dfn_f\)的影響\((low_u=dfn_f)\),特判起來會變得十分麻煩
故為了代碼的方便,在割點割邊時不考慮父親
邊雙連通分量
定義:簡寫為邊雙,同一邊雙內,點與點的邊集中無橋
如下圖,每種顏色的點為一個邊雙,之間由橋隔開
具體做法
-
兩次遍歷:這個就比較簡單了,直接找出所有的橋刪掉,然后遍歷一遍染色就行了,因為橋已經被全部刪掉,故每種顏色的分量的邊集中肯定無橋
-
一次遍歷:橋的定義入手,考慮橋\((f,u)\),\(u\)的子樹局限於內部,故滿足\(low_u=dfn_u\);而同屬\(u\)的邊雙內的任意點\(x\),由於無橋,肯定不會局限於\(x\)的子樹,故滿足\(low_x≠dfn_x\)。與強連通分量的做法類似,判斷\(dfn_u=low_u\),把壓進棧里的點取出來染色即可
void Tarjan(LL u,LL fa){
dfn[u]=low[u]=++tim; sta[++top]=u;
for(LL i=head[u];i;i=dis[i].nxt){
LL v(dis[i].to);
if(v==fa) continue;
if(!dfn[v]){
Tarjan(v,u); low[u]=std::min(low[u],low[v]);
}else low[u]=std::min(low[u],dfn[v]);
}
if(low[u]==dfn[u]){
LL now; ++nod;
do{
now=sta[top--]; col[now]=nod;
}while(now!=u);
}
}
點雙連通分量
定義:簡寫為點雙,對於同屬一個點雙的任意點,刪除后,該分量中的點仍能互相到達;或者說僅對於該分量而言,無割點。
具體做法:
依舊從割點的定義入手:割點將原圖分成互不相連的多個聯通塊,顯然每個聯通塊本身已經是一個點雙了,但不完整,因為相鄰的割點在邊界,如果與聯通塊共同組成一個新聯通塊,割掉后也不會另外產生聯通塊\((\)相當於該連通塊上的葉子節點\()\),所以需要加上來才完整\((\)故割點是會同時存在於多個點雙中的\()\)
我們怎么染色呢?可以發現在\(dfs\)樹中,割點\(u\)在進行與兒子節點的染色時會分給多個點雙,而在遍歷完兒子后,與祖先將僅產生一個完整點雙⑥。所以當\(u\)為割點,給兒子節點染色時,取到兒子借點就夠了,棧中保留\(u\),等到\(u\)的某個祖先后再取出來。
新建一個節點來維護某個點雙,該點向該點雙的每個點連一條邊\((\)當然這是建立在新圖上的\()\),這就是廣義圓方樹
⑥:如下圖
void Tarjan(LL u){
dfn[u]=low[u]=++tim; sta[++sta[0]]=u;
for(LL i=G1.head[u];i;i=G1.dis[i].next){
LL v(G1.dis[i].to);
if(!dfn[v]){
Tarjan(v);
low[u]=min(low[u],low[v]);
if(low[v]>=dfn[u]){
G2.Add(++tot,u); LL now(0);
do{
now=sta[sta[0]--];
G2.Add(tot,now);
}while(now!=v);
}
}else low[u]=min(low[u],dfn[v]);
}
}
例題一:[HNOI2012]礦場搭建
我們把每個點雙看作一個分量
-
分量無割點:說明整個聯通塊就是一個點雙,建兩個出口,隨便割一個由於點雙的性質所有點都能出去
-
分量有一個割點:在除割點的地方建一個出口,割掉割點直接去分量里的出口,割掉出口通過割點跑到其他分量的出口中
再具體點就相當於多個點雙構成了一棵樹\((\)僅一個節點的樹除外\()\),而我們僅在葉子節點建出口
例題二:[POI2008]BLO-Blockade
顯然僅割點會對除本身以外的訪問有影響,影響為多個分支所跨該割點訪問數,
記分支的節點數分別為\(size_1,size_2,...,size_k\),\(sum=\sum\limits_{i=1}^k size_i,ans=\sum\limits_{i=1}^k size_i\cdot(sum-size_i)+(n-1)*2\)
例題三:[ZJOI2004]嗅探器
其實就是求:多個點雙構成的樹,\(x,y\)所在節點間的割點
-
首先得所屬節點不同:\(low_y>=dfn_x\)
-
其次得保證\(u\)是位於期間的割點:\(dfn_u<=low_v\)
-
且\(u\)的該分支\(v\),\(y\)存在於子樹\(v\)內:\(dfn_v<=dfn_y\)
例題四:HDU - 5215
純奇環偶環通過dfs樹上,染色判斷(由於偶環可能有兩個奇環,通過一點相交,dfs樹上並不能判完)
兩環如果相交必定形成偶環,由於不可以重復經過邊,把每個邊雙提出來判斷一下是否存在兩個環以上即可