1. 程式人生 > 實用技巧 >強連通分量

強連通分量

今天聽了ztcdl的講解,隊友lkt,cyx帶了我幾道模板題,突然感覺自己行了,特寫下強連通分量板子。(可能自己還沒睡醒,有勇氣寫板子了)


強連通分量的預備姿勢:

①樹上的DFS序(時間戳):一句話,就是按照dfs的遍歷順序,把每個點再對應一個dfn陣列,dfn[i]存的就是dfs序的時間戳。

②DFS樹:就是在DFS時通向還沒有訪問過的點的那些邊所形成的樹。不在樹上的邊統稱為非樹邊,對於無向圖,就只有返祖邊;對於有向圖,有返祖邊、橫叉邊、前向邊。

黃色的為:返祖邊(指向其祖先)

藍色的為:前向邊(跨過兒子指孫子)

紅色的為:橫叉邊(指向別的子樹)

③強聯通的概念

例如圖一:所有點都可以走到這個強聯通分量中的任意一個點(屬於強聯通SCC)

圖二:顯然不滿足SCC

④縮點的思想

在找到強聯通之後,我們可以將一個強連通分量視為一個點,從而構造DAG。


SCC的程式碼理解:

我們發現,橫叉邊會影響判斷,所以應該直接刪去;前向邊不影響答案,可以無視它;只有返祖邊才會形成SCC。

const int maxn = 1e4 + 10;
vector<int>Map[maxn];
vector<int>a[maxn];
int n, m;
int low[maxn];      //low[i]表示從i出發能夠回到的最遠的祖先
int sccno[maxn];    //sccno[i]表示i屬於第sccno[i]個強連通分量
int dfn[maxn]; //DFS序就是DFS時某個點是第幾個被訪問的,用dfn[i]表示i的時間戳, int dfs_clock; //時間戳 int scc_cnt; //強連通分量的個數 stack<int>S; int num[maxn]; //num[i]表示第i個強連通分量中存在多少點 void tarjan(int u) //dfs(u)結束後 low[u]、pre[u]將會求出 { dfn[u] = low[u] = ++dfs_clock; S.push(u); for (int i = 0; i < Map[u].size(); i++)//
u->v { int v = Map[u][i]; if (!dfn[v]) //說明v還未被dfs { tarjan(v); //會自動求出low[v]、dfn[v] low[u] = min(low[u], low[v]); } else if (!sccno[v]) //說明v正在dfs:只求出了dfn[v] low[u] = min(low[u], dfn[v]); } if (dfn[u] == low[u]) //說明找到了一個強連通分量 { scc_cnt++; for (;;) { int x = S.top(); S.pop(); sccno[x] = scc_cnt; num[scc_cnt]++; a[scc_cnt].push_back(x); if (x == u)break; } } } void find_scc() { dfs_clock = scc_cnt = 0; memset(dfn, 0, sizeof(dfn)); memset(low, 0, sizeof(low)); memset(sccno, 0, sizeof(sccno)); for (int i = 1; i <= n; i++) if (!dfn[i]) //dfn[i] == 0 說明還沒走第i個點 tarjan(i); }

可以手模一下下圖:

先應該將邊3->4,5->6直接刪去

之後,初始化棧為empty

①1進入,dfn[1]=low[1]=++cnt=1
棧:1
②1->2 dfn[2]=low[2]=++cnt=2
棧: 1 2
③2->4 dfn[4]=low[4]=++cnt=3
棧: 1 2 4
④4->6 dfn[6]=low[6]=++cnt=4
棧: 1 2 4 6

6無出度,dfn[6]==low[6]
說明6是SCC的根節點

回溯到4後發現4找到了一個已經在棧中的點1,更新 low[4]

於是 low[4]=1

由4繼續回到2 low[2]=1
由2繼續回到1 low[1]=1

另一支,low[5]=dfn[5]=6
由5繼續回到3 low[3]=5

由3繼續回到1 low[1]=1

畫圖更快:(橙色為dfn,藍色為low)


例題:POJ1236Network of Schools