從寒假一開始,到如今也學習了兩個多星期的圖論中dfs的相關算法,也作了一些題目。在這裏先把強連通分支及其應用作一個第一階段總結,鞏固一下也便於開始下一步學習。在這裏我也會列出我總結的一套模版。算法
首先咱們要明確下面的這些算法都是針對有向圖而言的,先籠統的說一下強連通分支是什麼?其實就是有向圖中的一部分,在這部分裏任意兩個節點都相互可達。雖然表述可能不規範,可是應該比較形象吧。數組
1、強連通分支(scc)學習
接下來咱們先來學習一下如何在一張給定的圖中求出強連通分支,咱們須要介紹兩個算法:spa
(1)Kosaraju算法code
這個算法的思想很簡單,也比較好寫。前提是咱們已經熟練掌握了dfs的寫法及思想。下面是算法流程:blog
整個算法就描述完了看起來很簡單吧,接下來咱們說一下具體到程序中咱們該如何實現。ci
下面是個人模版:it
1 /**************************************** 2 強連通分支 kosaraju算法 3 By 小豪 4 *****************************************/ 5 const int LEN = 10000+10; 6 vector<int> Map[LEN], rMap[LEN], vs; 7 int vis[LEN], sccn[LEN], n, m; 8 9 void dfs(int v) 10 { 11 vis[v] = 1; 12 for(int i=0; i<Map[v].size(); i++) 13 if(!vis[Map[v][i]])dfs(Map[v][i]); 14 vs.PB(v); 15 } 16 17 void rdfs(int v, int k) 18 { 19 vis[v] = 1; 20 sccn[v] = k; 21 for(int i=0; i<rMap[v].size(); i++) 22 if(!vis[rMap[v][i]])rdfs(rMap[v][i], k); 23 } 24 25 int scc() 26 { 27 memset(vis, 0, sizeof vis); 28 vs.clear(); 29 for(int i=1; i<=n; i++) if(!vis[i])dfs(i); 30 memset(vis, 0, sizeof vis); 31 int k = 0; 32 for(int i=vs.size()-1; i>=0; i--) if(!vis[vs[i]]) rdfs(vs[i], k++); 33 return k; 34 }
算法複雜度是線性的, 返回的k記錄了有幾個強連通分支。sccn記錄了強連通分支的拓撲序(這頗有用)。io
(2)Tarjan算法class
tarjan是一個神奇的人,他提出了許多算法,而這個只是其中的一種。tarjan的複雜度也是線性的,並且比上一種更快由於他只要dfs一遍。tarjan是經過在搜索樹中發現第一個屬於這個強連通分支的結點,而後因爲只要和這個結點在一個強連通分支內的必定是他的後代,那麼如何判斷一點是否是強連通分支內第一個被發現的點呢?
這個問題咱們又要用到了low值與反向邊(和割點割邊的求法都很是相似)咱們在整個搜索過程當中設置一個dclock(稱爲時間戳)每搜過一個點就加一,這樣咱們就能在確認搜索樹中祖先與孩子的關係。比較明顯最早搜到的必定是根節點dfn(搜索標號)值爲1,稍微想一下就可發現dfn越小的節點越靠近根。
那麼low值又是什麼?簡單來講就是這個點經過反向邊能連到dfn最小的點(換句話說就是最靠近根節點的點)這樣如果一個點u經過他的孩子節點v能連會(注意這裏的連回指的是隻經過本身強連通份量內的點)u的祖先x,那麼咱們就能夠確認u,v,x在一個強連通分支內。如果u經過v最多隻能連回本身那麼咱們就能知道u是咱們第一個找到的點,在回溯是咱們就要把棧中結點取出(並非全取出具體參考代碼)記錄爲同一個強連通分支。
接線來問題又來了,咱們如何獲得一個節點dfn和low的值。先說dfn吧,很簡單隻要在dfs開始讓他等於時間戳就ok。而後是low首先咱們在dfs重要判斷接下來要走的即是屬於樹邊仍是反向邊,如果樹邊則說明是孩子節點,那必須用孩子節點的low值來更新當前節點的low值。如果反向邊,那說明是祖先,那隻要用祖先節點的dfn值來更新當前節點的low值。這樣low值就搞定的。
說了那麼多下面看代碼:
1 /**************************************** 2 強連通分支 tarjan算法 3 By 小豪 4 *****************************************/ 5 const int LEN = 100000 + 10; 6 vector<int> Map[LEN]; 7 int dfn[LEN], low[LEN], dclock, scc_cnt, sccn[LEN], n, m; 8 stack<int> s; 9 10 void sccinit(){ 11 for(int i=0; i<LEN; i++) Map[i].clear(); 12 while(!s.empty())s.pop(); 13 dclock = scc_cnt = 0; 14 memset(sccn, 0, sizeof sccn); 15 memset(dfn, 0, sizeof dfn); 16 } 17 18 void dfs(int u){ 19 dfn[u] = low[u] = ++dclock; 20 s.push(u); 21 for(int i=0; i<Map[u].size(); i++){ 22 int v = Map[u][i]; 23 if(!dfn[v]){ 24 dfs(v); 25 low[u] = min(low[u], low[v]); 26 }else if(!sccn[v]) low[u] = min(low[u], dfn[v]); 27 } 28 if(low[u] == dfn[u]){ 29 scc_cnt++; 30 while(1){ 31 int x = s.top();s.pop(); 32 sccn[x] = scc_cnt; 33 if(x == u) break; 34 } 35 } 36 }
兩端代碼長度是不相上下的都不長,在tarjan中咱們新增了scc_cnt來做爲全局變量記錄強連通分支的個數。
2、2-SAT
強聯通份量一個很重要的用途就是解布爾方程可知足性問題(SAT)。須要學習這一部分知識咱們須要一點布爾代數的知識。
下文中咱們約定(^表示交v表示並)
例如:(a v b v …)^(c v d v …)^…
這樣的咱們叫作合取範式。其中(a v b v …)這樣的叫作子句。相似a,b...叫作文字。
咱們把合取範式中一個子句中包含文字不超過兩個的問題成爲2-SAT問題。在SAT問題中只有這一類咱們能夠用線性時間內得出答案。
最常規的2-SAT題目分爲大體兩種,一種是讓你判斷有沒有解,另外一種是讓你輸出一組解。針對這兩種給出模版。
在這以前先來介紹一下2-SAT題目的大體解題步驟:
對於2-SAT問題咱們須要構建一張有向圖每一個文字拆爲兩個節點 例如 a 變爲 a, !a
首先咱們從題目中總結出來的都是一些比較雜亂的邏輯表達式,不過通常都是兩兩之間的關係,咱們須要作的第一步是化簡成用^鏈接。而後對於每一個子句建邊。
建邊的規則是這樣的 a -> b那麼在有向圖中建一條a到b的邊
咱們可能獲得的子句有:
a v b 咱們能夠化簡 !a->b ^ !b->a
a -> b 直接連邊
a 轉化爲!a -> a
其中每一個文字及其的非對應相應的結點,如果出如今文字前有非的關係例如 !a v b 那麼變通一下 就化成 a -> b ^ !b -> !a就能夠了。
到這裏咱們要作的事(建圖)就完成了,接下來交給模版,咱們來看一下模版作了什麼:
首先咱們對建完的有向圖求強連通分支,如果出現有一個邏輯變量和他的反在同一個聯通分以內就無解,不然有解。
若a所在的強連通分支的拓撲序在!a以後a爲真,不然爲反。怎麼樣很簡單吧。
下面貼出代碼:
1 /**************************************** 2 2-SAT kosaraju算法 3 By 小豪 4 *****************************************/ 5 const int LEN = 200000+10; 6 vector<int> Map[LEN], rMap[LEN], vs; 7 int n, m, vis[LEN], sccn[LEN]; 8 9 void dfs(int v){ 10 vis[v] = 1; 11 for(int i=0; i<Map[v].size(); i++) 12 if(!vis[Map[v][i]]) dfs(Map[v][i]); 13 vs.PB(v); 14 } 15 16 void rdfs(int v, int f){ 17 vis[v] = 1; 18 sccn[v] = f; 19 for(int i=0; i<rMap[v].size(); i++) 20 if(!vis[rMap[v][i]]) rdfs(rMap[v][i], f); 21 } 22 23 int scc(){ 24 memset(vis, 0, sizeof vis); 25 vs.clear(); 26 for(int i=0; i<2*n; i++) if(!vis[i]) dfs(i); 27 memset(vis, 0, sizeof vis); 28 int k = 0; 29 for(int i = vs.size()-1; i>=0; i--) if(!vis[vs[i]]) rdfs(vs[i], k++); 30 return k; 31 } 32 33 void addedge(int a, int b){ 34 Map[a].PB(b); 35 rMap[b].PB(a); 36 } 37 38 void solve() 39 { 40 scc(); 41 for(int i=0; i<2*n; i+=2) 42 if(sccn[i] == sccn[i+1]){ 43 //printf("No solution.\n"); 44 //無解 45 return ; 46 } 47 for(int i=0; i<n; i++){ 48 if(sccn[i*2] > sccn[i*2+1]) printf("Yes\n"); 49 else printf("No\n"); 50 } 51 }
好了主體部分講完了,接下來在講一下再強連通的題目中,咱們每每會用到縮點(就是把同一個強連通分支內的點縮成一個),其實縮點並非都要把幾個點縮成一個,須要根據題目的須要,有時候只需判斷一下就能夠了。在縮點後強連通分支內的點每每具備相同的特性,就賦予這個點一個新的意義。並且原圖也變成了DAG,就能夠dp等等。
水平有限,只但願把本身所知道的和你們分享一下。若大神發現有什麼錯誤,歡迎留言指正,定當感激涕零! By 張小豪