基本圖論-連通分量(強/弱聯通 割點/邊 邊/點雙)


前言

網上現存\(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樹上並不能判完)

兩環如果相交必定形成偶環,由於不可以重復經過邊,把每個邊雙提出來判斷一下是否存在兩個環以上即可

code


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM