连通性相关
強聯通分量
強連通:有向圖 \(G\) 強連通表示,\(G\) 中任意兩個結點連通。
強連通分量( Strongly Connected Components ,簡稱 \(\operatorname{SCC}\) ):極大的 強連通子圖。
Tarjan
維護了以下兩個變量:
-
\(dfn\) :深度優先搜索遍歷時結點 \(u\) 被搜索的次序 。
-
\(low\) :設以 \(u\) 為根的子樹為 \(subtree(u)\) 。 \(low\) 定義為以下結點的 \(dfn\) 的最小值: \(subtree(u)\) 中的結點;從 \(subtree(u)\) 通過 一條 不在搜索樹上的邊能到達的結點 。
從根開始的一條路徑上的 \(dfn\) 嚴格遞增,\(low\) 嚴格非降。
對于一個連通分量圖,我們很容易想到,在該連通圖中有且僅有一個 \(dfn[u]=low[u]\) 。該結點一定是在深度遍歷的過程中,該連通分量中第一個被訪問過的結點,因為它的 \(dfn\) 值和 \(low\) 值最小,不會被該連通分量中的其他結點所影響。
因此,在回溯的過程中,判定 \(dfn[u]=low[u]\) 的條件是否成立,如果成立,則棧中從 后面的結點構成一個 \(\operatorname{SCC}\) 。
P2341 [HAOI2006]受歡迎的牛 G \(-\) 模板
$\texttt{code}$ #define Maxn 10005 #define Maxm 50005 void tarjan(int u) {dfn[u]=low[u]=++Time; s.push(u),ins[u]=true;for(int i=hea[u];i;i=nex[i]){if(!dfn[ver[i]]) tarjan(ver[i]),low[u]=min(low[ver[i]],low[u]);else if(ins[ver[i]]) low[u]=min(dfn[ver[i]],low[u]);}if(dfn[u]==low[u]){sum+=1;do{belong[u]=sum;u=s.top(); s.pop(); ins[u]=false;cnt[sum]+=1;} while(dfn[u]!=low[u]);} }for(int i=1;i<=n;i++) if(!dfn[i]) tarjan(i);時間復雜度 \(O(n+m)\) 。
Kosaraju
復雜度 \(O(n+m)\) 。
Garbow
復雜度 \(O(n+m)\) 。
我們可以利用強聯通分量將一張圖的每個強連通分量都縮成一個點。
然后這張圖會變成一個 \(\operatorname{DAG}\),可以進行拓撲排序以及更多其他操作 。
應用 \(-\) 縮點
P3387 【模板】縮點
for(int i=1;i<=n;i++) if(!dfn[i]) tarjan(i); for(int i=1;i<=tot[0];i++)if(belong[fro[0][i]]!=belong[ver[0][i]])add(1,belong[fro[0][i]],belong[ver[0][i]]),ind[belong[ver[0][i]]]++; topo();割點與橋
在無向圖中刪去這個點 \(/\) 邊會使極大強聯通增大,那么這個點 \(/\) 邊為割點 \(/\) 橋 。
注意這里的 \(dfn\) 表示不經過父親,能到達的最小的 \(dfn\) 。
割點
P3388 【模板】割點(割頂)
關鍵條件:
-
若 \(u\) 是根節點,當至少存在 \(2\) 條邊滿足 \(low[v] >= dfn[u]\) 則 \(u\) 是割點 。
-
若 \(u\) 不是根節點,當至少存在 \(1\) 條邊滿足 \(low[v] >= dfn[u]\) 則 \(u\) 是割點 。
割邊(橋)
關鍵條件:
- 當存在一條邊條邊滿足 \(low[v] > dfn[u]\) 則邊 \(i\) 是割邊
關鍵部分的代碼:
注意:記錄上一個訪問的邊時要記錄邊的編號,不能記錄上一個過來的節點(因為會有重邊)!!!
$\texttt{code}$ void tarjan(int x,int Last_edg) {dfn[x]=low[x]=++Time;for(int i=hea[x];i;i=nex[i]){if(!dfn[ver[i]]){tarjan(ver[i],i);low[x]=min(low[x],low[ver[i]]);if(low[ver[i]]>dfn[x]) edg[i]=edg[i^1]=1;}else if(i!=(Last_edg^1)) low[x]=min(low[x],dfn[ver[i]]);} }for(int i=1;i<=n;i++) if(!dfn[i]) tarjan(i,0); for(int j=2;j<=tot;j+=2) ans+=tag[j];雙聯通分量
邊雙聯通分量
顯然,找出每一個橋,去掉這些橋之后的每一個聯通塊都是一個邊雙聯通字圖。
注意:用邊雙縮點的時候先處理出割邊,之后用 \(\text{dfs}\) 求出每一個雙聯通分量,不用棧!!
例題:P2860 [USACO06JAN]Redundant Paths G
$\texttt{solution}$一句話題意:要將原圖轉化為邊雙聯通圖需要添加的最少邊數
我們可以先將所有的橋找出來,并同時對所有邊雙縮點,會得到一顆縮完點的、由橋構成的“樹”。
我們發現這棵“樹”上“葉子結點”的個數除二向上取整就是需要添加的邊的條數。
點雙連通分量
小粉兔的圓方樹——點雙詳解
【模板】點雙連通分量
回憶 \(low\) 的定義,就是 \(x\) 的子樹內最多經過一條反祖邊或一條向父親的邊能夠到達的最小的 \(dfn\) 值。
當 \(x\) 不是這個連通塊的根時,如果存在一條邊滿足 \(low(ver)\ge dfn(x)\),那么 \(x\) 就是一個個點。
而 \(x\) 是根時,\(x\) 需要存在至少兩條邊滿足以上條件。
這是因為當 \(x\) 為根時,沒有父親與之相連。
那么當我們在求點雙時,只需要判斷子樹內的 \(low\) 是否會 \(<dfn(x)\),若會,則說明子樹中的點能夠到達 \(x\) 的祖先,\(x\) 必然不是個點。而若不會,即 \(low(ver)\ge dfn(x)\),則說明 \(x\) 是這個點雙的頂端個點,這時可以將遞歸進入的點都彈出,直至棧頂元素變為 \(ver\),再將 \(ver\) 從棧中彈出,加上 \(x\) 就是這個點雙聯通分量了。
還要注意孤立點的情況。
如果需要處理點雙中點的個數,那么可以在棧中存放點,例如一下代碼:
void tarjan(int x) {dfn[x]=low[x]=++Time,sta[++tp]=x;if(tp==1 && !hea[x]) { SCC[++sum].pb(x); return; }for(int i=hea[x];i;i=nex[i]){if(!dfn[ver[i]]){tarjan(ver[i]);low[x]=min(low[x],low[ver[i]]);if(low[ver[i]]>=dfn[x]){sum++;do { SCC[sum].pb(sta[tp--]); }while(sta[tp+1]!=ver[i]);SCC[sum].pb(x);}}else low[x]=min(low[x],dfn[ver[i]]);} }而如果需要處理點雙中的邊,那么就需要在棧中存邊,如以下代碼:
void tarjan(int x,int fa) {dfn[x]=low[x]=++Time;int cntson=0;for(int i=hea[x];i;i=nex[i]){if(!dfn[ver[i]]){sta[++tp]=i,tarjan(ver[i],i),cntson++;low[x]=min(low[x],low[ver[i]]);if(low[ver[i]]>=dfn[x]){isdian[x]=true,sum++;int tmp=0;do{tmp=sta[tp--];scc[sum].pb(fro[tmp]);scc[sum].pb(ver[tmp]);}while(tmp!=i);}}else if(i!=(fa^1)) low[x]=min(low[x],dfn[ver[i]]); }if(fa==0 && cntson==1) isdian[x]=false; }總結
- 上一篇: OPPO A2 手机官宣 11 月 11
- 下一篇: 期望 概率DP