強連通分支及其應用(2-SAT)總結

從寒假一開始,到如今也學習了兩個多星期的圖論中dfs的相關算法,也作了一些題目。在這裏先把強連通分支及其應用作一個第一階段總結,鞏固一下也便於開始下一步學習。在這裏我也會列出我總結的一套模版。算法

首先咱們要明確下面的這些算法都是針對有向圖而言的,先籠統的說一下強連通分支是什麼?其實就是有向圖中的一部分,在這部分裏任意兩個節點都相互可達。雖然表述可能不規範,可是應該比較形象吧。數組

1、強連通分支(scc)學習

接下來咱們先來學習一下如何在一張給定的圖中求出強連通分支,咱們須要介紹兩個算法:spa

(1)Kosaraju算法code

這個算法的思想很簡單,也比較好寫。前提是咱們已經熟練掌握了dfs的寫法及思想。下面是算法流程:blog

  • 首先咱們對原圖先進性一邊dfs獲得原圖中各結點的拓撲序把他存在一個數組裏。
  • 在有了拓撲序後,咱們再對原圖反向後的圖按照逆拓撲序進行dfs每次dfs就獲得一個強連通分支。

整個算法就描述完了看起來很簡單吧,接下來咱們說一下具體到程序中咱們該如何實現。ci

  • 準備 有上面兩步咱們看到不只須要原圖咱們還須要原圖中全部邊都反向的圖,因此咱們在處理輸入時必須同時獲得Map,rMap。
  • 初始化 很簡單,vis數組置零(dfs標記用),vs數組清空(記錄拓撲序用)。
  • 開始第一遍dfs 沒遍歷一個節點在回溯時將其加入拓撲序數組。
  • 開始第二遍dfs 記得以前清空vis數組。而且此次咱們要在參數中加入一項就是f標記dfs到的結點是屬於那個強連通分支的(記錄在sccn數組中)。

下面是個人模版: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 張小豪

相關文章
相關標籤/搜索