強連通分量
今天聽了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