1. 程式人生 > >圖的連通性問題

圖的連通性問題

基本概念

無向圖

  • 連通圖和非聯通圖: 如果無向圖 G 中任意一對頂點都是連通的,則稱此圖是連通圖(connected graph);相反,如
    果一個無向圖不是連通圖,則稱為非連通圖(disconnected graph)。對非連通圖G,其極大連通子圖稱為連通分量(connected component,或連通分支),連通分支數記為w(G)。

  • 割頂集與連通度: 設V’是連通圖G 的一個頂點子集,在G 中刪去V’及與V’關聯的邊後圖不連通,則稱 V’ 是 G 的割頂集(vertex-cut set)。如果割頂集V’的任何真子集都不是割頂集,則稱V’為極小割頂 集。頂點個數最小的極小割頂集稱為最小割頂集。最小割頂集中頂點的個數,稱作圖G 的頂點連通度(vertex connectivity degree),記做κ(G),且稱圖G 是κ–連通圖(κ–connected graph)。

  • 割點:如果割頂集中只有一個頂點,則該頂點可以稱為割點(cut-vertex),或關節點。

  • 點雙連通圖:如果一個無向連通圖 G 沒有關節點,或者說點連通度κ(G) > 1,則稱 G 為點雙 連通圖,或者稱為重連通圖。

  • 點雙連通分量:一個連通圖 G 如果不是點雙連通圖,那麼它可以包括幾個點雙連通分量,也 稱為重連通分量(或塊)。一個連通圖的重連通分量是該圖的極大重連通子圖,在重連通分量中不存在關節點。

  • 割邊集與邊連通度:設 E’ 是連通圖 G 的邊集的子集,在 G 中刪去E’後圖不連通,則稱E’是G 的割邊集 (edge-cut set)。如果割邊集 E’ 的任何真子集都不是割邊集,則稱 E’ 為極小割邊集。邊數最小的極 小割邊集稱為最小割邊集。最小割邊集中邊的個數,稱作圖G 的邊連通度(edge connectivity degree),記做λ(G),且稱圖G 是λ–邊連通圖(λ–edge–connected graph)。

  • 割邊:如果割邊集中只有一條邊,則該邊可以稱為割邊(bridge),或橋。

  • 邊雙連通圖:如果一個無向連通圖 G 沒有割邊,或者說邊連通度λ(G) > 1,則稱G 為邊雙連通圖。

  • 邊雙連通分量:邊雙連通分量:一個連通圖 G 如果不是邊雙連通圖,那麼它可以包括幾個邊雙連通分量。一 個連通圖的邊雙連通分量是該圖的極大重連通子圖,在邊雙連通分量中不存在割邊。在連通圖中, 把割邊刪除,則連通圖變成了多個連通分量,每個連通分量就是一個邊雙連通分量。

  • 頂點連通性與邊連通性的關係:(頂點連通度、邊連通度與圖的最小度的關係) 設G 為無向連通圖,則存在關係式:

    κ(G)λ(G)δ(G)
  • 割邊和割點的聯絡

    :(割邊和割點的聯絡)設 v 是圖 G 中與一條割邊相關聯的頂點,則 v 是 G 的割點當且僅當

    deg(v)2

有向圖

  • 強連通(strongly connected):若 G 是有向圖,如果對圖 G 中任意兩個頂點 u 和 v,既存在從 u 到 v 的路徑,也存在從 v 到 u 的路徑,則稱該有向圖為強連通有向圖。對於非強連通圖,其極 大強連通子圖稱為其強連通分量。

  • 單連通(simply connected):若 G 是有向圖,如果對圖 G 中任意兩個頂點 u 和 v,存在從 u 到 v 的路徑或從 v 到 u 的路徑,則稱該有向圖為單連通有向圖。

  • 弱連通(weak connected):若 G 是有向圖,如果忽略圖 G 中每條有向邊的方向,得到的無向 圖(即有向圖的基圖)連通,則稱該有向圖為弱連通有向圖。

無向圖點連通性的求解及應用

求割點

Tarjan 演算法只需從某個頂點出發進行一次遍歷,就可以求得圖中所有的關節點,因此其複雜度為O(n^2)。接下來以圖(a)所示的無向圖為例介紹這種方法。
在圖(a)中,對該圖從頂點 4 出發進行深度優先搜尋,實線表示搜尋前進方向,虛線表示 回退方向,頂點旁的數字標明瞭進行深度優先搜尋時各頂點的訪問次序,即深度優先數。在 DFS 搜尋過程中,可以將各頂點的深度優先數記錄在陣列dfn 中。 圖(b)是進行DFS 搜尋後得到的根為頂點4 的深度優先生成樹。為了更加直觀地描述樹形結 構,將此生成樹改畫成圖(d)所示的樹形形狀。在圖(d)中,還用虛線畫出了兩條雖然屬於圖G、但 不屬於生成樹的邊,即(4, 5)和(6, 8)。 請注意:在深度優先生成樹中,如果u 和v 是2 個頂點,且在生成樹中u 是v 的祖先,則必 有dfn[u] < dfn[v],表明u 的深度優先數小於v,u 先於 v 被訪問。

圖G 中的邊可以分為3 種:

  • 1) 生成樹的邊,如(2, 4)、(6, 7)等。
  • 2) 回邊(back edge):圖(d)中虛線所表示的非生成樹的邊,稱為回邊。當且僅當 u 在生成樹中是 v 的祖先,或者 v 是 u 的祖先時,非生成樹的邊(u,v)才成為一條回邊。如圖(a)及圖(d)中的(4, 5)、(6, 8)都是回邊。
  • 3) 交叉邊:除生成樹的邊、回邊外,圖G 中的其他邊稱為交叉邊。
    請特別注意:一旦生成樹確定以後,那麼原圖中的邊只可能是回邊和生成樹的邊,交叉邊實際上是不存在的。為什麼?(說明:對有向圖進行DFS 搜尋後,非生成樹的邊可能是交叉邊) 假設圖G 中存在邊(1, 10),如圖(c)所示,這就是所謂的交叉邊,那麼頂點10(甚至其他頂點都)只能位於頂點4 的左邊這棵子樹中。另外,如果在圖G 中增加兩條交叉邊(1, 10)和(7, 9),則圖G 就是一個重連通圖,如圖(c)所示。

頂點u 是關節點的充要條件:
1) 如果頂點u 是深度優先搜尋生成樹的根,則u 至少有2 個子女。為什麼呢?因為刪除u,它的子女所在的子樹就斷開了,你不用擔心這些子樹之間(在原圖中)可能存在邊,因為交叉邊是不存在的。

2) 如果 u 不是生成樹的根,則它至少有一個子女 w,從 w 出發,不可能通過w、w 的子孫,以及一條回邊組成的路徑到達 u 的祖先。為什麼呢?這是因為如果刪除頂點 u 及其 所關聯的邊,則以頂點 w 為根的子樹就從搜尋樹中脫離了。例如,頂點6 為什麼是關節 點?這是因為它的一個子女頂點,如圖(d)所示,即頂點7,不存在如前所述的路徑到達頂點6 的祖先結點,這樣,一旦頂點6 刪除了,則以頂點7 為根結點的子樹就斷開了。 又如,頂點7 為什麼不是關節點?這是因為它的所有子女頂點,當然在圖(d)中只有頂點 8,存在如前所述的路徑到達頂點7 的祖先結點,即頂點6,這樣,一旦頂點7 刪除了, 則以頂點8 為根結點的子樹仍然跟圖G 連通。

因此,可對圖 G 的每個頂點 u 定義一個 low 值:low[u]是從 u 或 u 的子孫出發通過回邊可以到達的最低深度優先數。low[u]的定義如下:

low[u] = Min
{
    dfn[u],
    Min{ low[w] | w 是u 的一個子女}, (8-2)
    Min{ dfn[v] | v 與u 鄰接,且(u,v)是一條回邊 }
}

即low[u]是取以上三項的最小值,其中:

  • 第1 項為它本身的深度優先數;
  • 第2 項為它的(可能有多個)子女頂點w 的low[w]值的最小值,因為它的子女可以到達的最低深度優先數,則它也 可以通過子女到達;
  • 第 3 項為它直接通過回邊可以到達的最低優先數。

因此,頂點u 是關節點的充要條件是:u 或者是具有兩個以上子女的深度優先生成樹的根, 或者雖然不是一個根,但它有一個子女w,使得 low[w]>=dfn[u]。
其中,“low[w]>=dfn[u]”的含義是:頂點u 的子女頂點w,能夠通過如前所述的路徑到達頂 點的最低深度優先數大於等於頂點u 的深度優先數(注意在深度優先生成樹中,頂點m 是頂點n 的祖先,則必有dfn[m] < dfn[n]),即w 及其子孫不存在指向頂點u 的祖先的回邊。這時刪除頂點 u 及其所關聯的邊,則以頂點 w 為根的子樹就從搜尋樹中脫離了。 每個頂點的深度優先數dfn[n]值可以在搜尋前進時進行統計,而low[n]值是在回退的時候進行計算的。
接下來結合圖和表解釋在回退過程中計算每個頂點 n 的low[n]值的方法 (當前計算出來的low[n]值用粗體、斜體及下劃線標明):

  • 1) 在圖(a)中,訪問到頂點1 後,要回退,因為頂點1 沒有子女頂點,所以low[1]就等於它的深度優先數dfn[1],為5;
  • 2) 從頂點1 回退到頂點5 後,要繼續回退,此時計算頂點5 的low 值,因為頂點5 可以直接通過回邊(5, 4)到達根結點,而根結點的深度優先數為1,所以頂點5 的low 值為1;
  • 3) 從頂點5 回退到頂點3 後,要繼續回退,此時計算頂點3 的low 值,因為它的子女頂點, 即頂點5 的low 值為1,則頂點3 的low 值也為1;
  • 4) 從頂點3 回退到頂點2 後,要繼續回退,此時計算頂點2 的low 值,因為它的子女頂點, 即頂點3 的low 值為1,則頂點2 的low 值也為1;
  • 5) 從頂點2 回退到頂點4 後,要繼續訪問它的右子樹中的頂點,此時計算頂點4 的low 值,因為它的子女頂點,即頂點2 的low 值為1,則頂點4 的low 值也為1; 根結點4 右子樹在回退過程計算頂點的low[n],方法類似。

    求出關節點u 後,還有一個問題需要解決:去掉該關節點u,將原來的連通圖分成了幾個連通分量?答案是:

  • 1) 如果關節點 u 是根結點,則有幾個子女,就分成了幾個連通分量;

  • 2) 如果關節點 u 不是根結點,則有 d 個子女 w ,使得low[w] >= dfn[u],則去掉該結點,分成了d + 1 個連通分量。
    Tarjan實現Code:
// 當 res[i] > 0 時說明 i 是割點, 並且去掉 i 之後圖的連通分量的個數為 res[i];
void Tarjan(int u, int fa){
    int son = 0;
    dfn[u] = low[u] = ++tdfn;
    for(int i = head[u]; i + 1; i = edge[i].nxt){
        int v = edge[i].v;
        if((fa ^ 1) == i) continue;
        if(!dfn[v]){
            Tarjan(v, i);
            low[u] = min(low[u], low[v]);
            if(low[v] >= dfn[u]) son++;
        }else low[u] = min(low[u], dfn[v]);
    }
    if(u != root && son) res[u] = son + 1;
    else if(u == root && son > 1) res[u] = son;
    else res[u] = 0;
}
// 呼叫時
root = 1;

點雙連通分量的求解

在求關節點的過程中就能順便把每個重連通分量求出。方法是:建立一個棧,儲存當前重連通分量,在 DFS 過程中,每找到一條生成樹的邊或回邊,就把這條邊加入棧中。如果遇到某個頂點 u 的子女頂點 v 滿足 dfn[u] <= low[v],說明 u 是一個割點,同時把邊從棧頂一條條取出,直到遇到了邊(u, v),取出的這些邊與其關聯的頂點,組成一個重連通分量。割點可以屬於多個重連通分量,其餘頂點和每條邊屬於且只屬於一個重連通分量。

// bcc_cnt 即連通分支數目
void Tarjan(int u, int fa){
    sta.push(u);
    instack[u] = true;
    low[u] = dfn[u] = ++tdfn;
    for(int i = head[u]; i + 1; i = edge[i].nxt){
        int v = edge[i].v;
        if((fa ^ 1) == i) continue;
        if(!dfn[v]){
            Tarjan(v, i);
            low[u] = min(low[u], low[v]);
        }else low[u] = min(low[u], dfn[v]);
    }
    if(low[u] == dfn[u]){
        bcc_cnt++;
        int top;
        do{
            top = sta.top();
            sta.pop();
            instack[top] = false;
            belong[top] = bcc_cnt;
        }while(u != top);
    }
}

割邊的求解

割邊的求解過程與求割點的過程類似,判斷方法是:無向圖中的一條邊(u, v)是橋,當且僅當(u, v)為生成樹中的邊,且滿足dfn[u] < low[v]。
例如,圖(a)所示的無向圖,如果從頂點 4 開始進行DFS 搜尋,各頂點的 dfn[] 值和 low[] 值如圖(a)所示(每個頂點旁的兩個數值分別表示 dfn[] 值和 low[] 值),深度優先搜尋樹如圖(b)所 示。根據上述判斷方法,可判斷出邊(1, 5)、(4, 6)、(8, 9)和(9, 10)為無向圖中的割邊。

// 求橋的模板, res陣列儲存的是橋的編號
void Tarjan(int u, int fa){
    low[u] = dfn[u] = ++tdfn;
    for(int i = head[u]; i + 1; i = edge[i].nxt){
        int v = edge[i].v;
        if((fa ^ 1) == i) continue;
        if(!dfn[v]){
            Tarjan(v, i);
            low[u] = min(low[u], low[v]);
            if(low[v] > dfn[u]) res[cnt++] = edge[i].id;
        }else low[u] = min(low[u], dfn[v]);
    }
}
// 呼叫時  Tarjan(1, -1);

邊雙連通分量的求解

在求出所有的橋以後,把橋刪除,原圖變成了多個連通塊,則每個連通塊就是一個邊雙連通分量。橋不屬於任何一個邊雙連通分量,其餘的邊和每個頂點都屬於且只屬於一個邊雙連通分量。

邊連通度的求解

有向圖強連通性的求解及應用

有向圖強連通分量的求解演算法

Tarjan 演算法

Tarjan 演算法是基於 DFS 演算法,每個強連通分量為搜尋樹中的一棵子樹。搜尋時,把當前搜尋 樹中未處理的節點加入一個棧,回溯時可以判斷棧頂到棧中的節點是否為一個強連通分量。當 dfn(u) = low(u)時,以 u 為根的搜尋子樹上所有節點是一個強連通分量。
接下來以圖(a)所示的有向圖為例解釋 Tarjan 演算法的思想和執行過程,在該有向圖中,{ 1, 2, 5, 3 }為一個強連通分量,{ 4 }、{ 6 }也分別是強連通分量。 圖(b)為從頂點1 出發進行深度優先搜尋後得到的深度優先搜尋樹。約定:如果某個頂點有多 個未訪問過的鄰接頂點,按頂點序號從小到大的順序進行選擇。各頂點旁邊的兩個數值分別為頂 點的深度優先數(dfn[])值和low[]值。在圖(b)中,虛線表示非生成樹的邊,其中邊<5, 6="">為交 叉邊,邊<5, 1="">和<3, 5="">是回邊

圖(c)~(f)演示了 Tarjan 演算法的執行過程。在圖(c)中,沿著實線箭頭所指示的方向搜尋到頂點 6,此時無法再前進下去了,並且因為此時 dfn[6] = low[6] = 4,所以找到了一個強連通分量。退棧到u == v 為止,{ 6 }為一個強連通分量。
在圖(d)中,沿著虛線箭頭所指示的方向回退到頂點4,發現dfn[4] == low[4],為3,退棧後{ 4 } 為一個強連通分量。
在圖(e)中,回退到頂點2 並繼續搜尋到頂點5,把頂點5 加入棧。發現頂點5 有到頂點 1 的有向邊,頂點 1 還在棧中,所以 low[5] = 1,有向邊 <5, 1=""> 為回邊。頂點 6 已經出棧,所以 <5, 6=""> 是交叉邊,返回頂點 2,<2, 5="">為生成樹的邊,所以low[2] = low[5] = 1。
在圖(f)中,先回退到頂點 1,接著訪問頂點 3。發現頂點 3 到頂點有一條有向邊,頂點 5 已經訪問過了、且 5 還在棧中,因此邊 <3, 5=""> 為回邊,所以low[3] = dfn[5] = 5。返回頂點 1 後,發現 dfn[1] == low[1],把棧中的頂點全部彈出,組成一個連通分量{ 3, 5, 2, 1 }。 至此,Tarjan 演算法結束,求出了圖中全部的三個強連通分量為{ 6 }、{ 4 }和{ 3, 5, 2, 1 }。
Tarjan 演算法的時間複雜度分析:假設用鄰接表儲存圖,在 Tarjan 演算法的執行過程中,每個頂點都被訪問了一次,且只進出了一次棧,每條邊也只被訪問了一次,所以該演算法的時間複雜度為 O(n + m)。

// 程式碼模板
void Tarjan(int u){
    sta.push(u);
    instack[u] = true;
    dfn[u] = low[u] = ++tdfn;
    for(int i = head[u]; i + 1; i = edge[i].nxt){
        int v = edge[i].v;
        if(!dfn[v]){
            Tarjan(v);
            low[u] = min(low[u], low[v]);
        }else if(instack[v]) low[u] = min(low[u], dfn[v]);
    }
    if(low[u] == dfn[u]){
        int top;
        scc_cnt++;
        do{
            top = sta.top();
            sta.pop();
            instack[top] = false;
            belong[top] = scc_cnt;
        }while(u != top);
    }
}

// 呼叫
for(int i = 1; i <= n; i++)

Kosaraju 演算法

Kosaraju 演算法是基於對有向圖 G 及其逆圖 GT(各邊反向得到的有向圖)進行兩次 DFS 的方 法,其時間複雜度也是 O(n + m)。與 Trajan 演算法相比,Kosaraju 演算法的思想更為直觀。
Kosaraju 演算法的原理為:如果有向圖 G 的一個子圖 G’ 是強連通子圖,那麼各邊反向後沒有任何影響,G’ 內各頂點間仍然連通,G’ 仍然是強連通子圖。但如果子圖G’是單向連通的,那麼各邊反向後可能某些頂點間就不連通了,因此,各邊的反向處理是對非強連通塊的過濾。
Kosaraju 演算法的執行過程為:

  • (1) 對原圖G 進行深度優先搜尋,並記錄每個頂點的 dfn[] 值。
  • (2) 將圖G 的各邊進行反向,得到其逆圖GT。
  • (3) 選擇從當前dfn[ ]值最大的頂點出發,對逆圖GT 進行DFS 搜尋,刪除能夠遍歷到的頂點,這些頂點構成一個強連通分量。
  • (4) 如果還有頂點沒有刪除,繼續執行第(3)步,否則演算法結束。
    接下來以圖(a)所示的有向圖 G 為例分析 Kosaraju 演算法的執行過程。圖(b)為正向搜尋過程,搜尋完畢後,得到各頂點的 dfn[ ]值。圖(c)為逆圖GT。圖(d)為從頂點3 出發對逆圖GT 進行 DFS 搜尋,得到第1 個強連通分量{ 1, 2, 5, 3 },圖(e)和(f)分別從頂點4 和6 出發進行DFS 搜尋得到另外兩個強連通分量。
// 演算法模板
void dfs(int u){
    vis[u] = true;
    for(int i = head[u]; i + 1; i = edge[i].nxt)
        if(!vis[edge[i].v]) dfs(edge[i].v);
    vs[vscnt++] = u;
}

void rdfs(int u, int k){
    vis[u] = true;
    belong[u] = k;
    for(int i = rhead[u]; i + 1; i = redge[i].nxt)
        if(!vis[redge[i].v]) rdfs(redge[i].v, k);
}

int scc(){
    memset(vis, 0, sizeof(vis));
    vscnt = 0;
    for(int i = 1; i <= n; i++)
        if(!vis[i]) dfs(i);

    int scc_cnt = 0;
    memset(vis, 0, sizeof(vis));
    for(int i = vscnt - 1; i >= 0; i--)
        if(!vis[vs[i]]) rdfs(vs[i], scc_cnt++);
    
    return scc_cnt;
}

連通性演算法的應用2_SAT

簡介

現有一個由 N 個布林值組成的序列 A,給出一些限制關係,比如 A[x] AND A[y]=0 A[x] OR A[y] OR A[z] = 1 等,要確定 A[0..N-1] 的值,使得其滿足所有限制關係。這個稱為 SAT 問題,特別的,若每種限制關係中最多隻對兩個元素進行限制,則稱為 2-SAT 問題。

展開

由於在2-SAT問題中,最多隻對兩個元素進行限制,所以可能的限制關係共有11種:
A[x]
NOT A[x]
A[x] AND A[y]
A[x] AND NOT A[y]
A[x] OR A[y]
A[x] OR NOT A[y]
NOT (A[x] AND A[y])
NOT (A[x] OR A[y])
A[x] XOR A[y]
NOT (A[x] XOR A[y])
A[x] XOR NOT A[y]
進一步,A[x] AND A[y]相當於(A[x]) AND (A[y])(也就是可以拆分成A[x]與A[y]兩個限制關係),NOT(A[x] OR A[y])相當於NOT A[x] AND NOT A[y](也就是可以拆分成NOT A[x]與NOT A[y]兩個限制關係)。因此,可能的限制關係最多隻有9種。

在實際問題中,2-SAT問題在大多數時候表現成以下形式:有N對物品,每對物品中必須選取一個,也只能選取一個,並且它們之間存在某些限制關係(如某兩個物品不能都選,某兩個物品不能都不選,某兩個物品必須且只能選一個,某個物品必選)等,這時,可以將每對物品當成一個布林值(選取第一個物品相當於0,選取第二個相當於1),如果所有的限制關係最多隻對兩個物品進行限制,則它們都可以轉化成9種基本限制關係,從而轉化為2-SAT模型。

建模

其實 2-SAT 問題的建模是和實際問題非常相似的。建立一個 2N 階的有向圖,其中的點分為 N 對,每對點表示布林序列 A 的一個元素的 0、1 取值(以下將代表 A[i] 的 0 取值的點稱為 i,代表 A[i] 的 1 取值的點稱為i’)。顯然每對點必須且只能選取一個。然後,圖中的邊具有特定含義。若圖中存在邊 ,則表示若選了 i 必須選 j。可以發現,上面的 9 種限制關係中,後7種二元限制關係都可以用連邊實現,比如NOT(A[x] AND A[y])需要連兩條邊,A[x] OR A[y]需要連兩條邊。而前兩種一元關係,對於A[x](即x必選),可以通過連邊來實現。

O(NM)演算法:求字典序最小的解

根據 2-SAT 建成的圖中邊的定義可以發現,若圖中 i 到 j 有路徑,則若 i 選,則 j 也要選;或者說,若 j 不選,則 i 也不能選;
因此得到一個很直觀的演算法:

  • (1)給每個點設定一個狀態 V,V = 0 表示未確定,V = 1 表示確定選取,V = 2 表示確定不選取。稱一個點是已確定的當且僅當其 V 值非 0。設立兩個佇列 Q1 和 Q2,分別存放本次嘗試選取的點的編號和嘗試不選的點的編號。

  • (2)若圖中所有的點均已確定,則找到一組解,結束,否則,將 Q1、Q2 清空,並任選一個未確定的點 i,將 i 加入佇列 Q1,將 i’ 加入佇列 Q2;

  • (3)找到 i 的所有後繼。對於後繼 j,若 j 未確定,則將 j 加入佇列 Q1;若 j’(這裡的 j’ 是指與 j 在同一對的另一個點)未確定,則將 j’ 加入佇列 Q2;

  • (4)遍歷 Q2 中的每個點,找到該點的所有前趨(這裡需要先建一個補圖),若該前趨未確定,則將其加入佇列 Q2;

  • (5)在(3)(4)步操作中,出現以下情況之一,則本次嘗試失敗,否則本次嘗試成功:

    • <1>某個已被加入佇列 Q1 的點被加入佇列 Q2;
    • <2>某個已被加入佇列 Q2 的點被加入佇列 Q1;
    • <3>某個 j 的狀態為 2;
    • <4>某個 i’ 或 j’ 的狀態為 1 或某個 i’ 或 j’ 的前趨的狀態為 1 ;
  • (6)若本次嘗試成功,則將Q1中的所有點的狀態改為1,將Q2中所有點的狀態改為2,轉(2),否則嘗試點i’,若仍失敗則問題無解。

該演算法的時間複雜度為 O(NM)(最壞情況下要嘗試所有的點,每次嘗試要遍歷所有的邊),但是在多數情況下,遠遠達不到這個上界。
具體實現時,可以用一個數組 vst 來表示佇列 Q1 和 Q2。設立兩個標誌變數 i1 和 i2(要求對於不同的 i,i1 和 i2 均不同,這樣可以避免每次嘗試都要初始化一次,節省時間),若 vst[i] = i1 則表示 i 已被加入 Q1,若 vst[i] = i2 則表示 i 已被加入 Q2。不過 Q1 和 Q2 仍然是要設立的,因為遍歷(BFS)的時候需要佇列,為了防止重複遍歷,加入 Q1(或Q2)中的點的 vst 值必然不等於 i1(或i2)。中間一旦發生矛盾,立即中止嘗試,宣告失敗。

該演算法雖然在多數情況下時間複雜度到不了 O(NM),但是綜合性能仍然不如下面的 O(M) 演算法。不過,該演算法有一個很重要的用處:求字典序最小的解!
如果原圖中的同一對點編號都是連續的(01、23、45……)則可以依次嘗試第 0 對、第 1 對……點,每對點中先嚐試編號小的,若失敗再嘗試編號大的。這樣一定能求出字典序最小的解(如果有解的話),因為一個點一旦被確定,則不可更改。
如果原圖中的同一對點編號不連續(比如03、25、14……)則按照該對點中編號小的點的編號遞增順序將每對點排序,然後依次掃描排序後的每對點,先嚐試其編號小的點,若成功則將這個點選上,否則嘗試編號大的點,若成功則選上,否則(都失敗)無解。

只輸出一組可行解(O(n + m))

根據《挑戰程式設計競賽》的說法,如果不存在 x 與 NOTx 同在一個強連通分量, 那麼對於每一個布林變數 x , 讓

xNOTx<=>x 就是使得該公式的值為真的一組合適的布林變數賦值。
Blog:    https://blog.andrewei.info/2016/04/06/Connectivity-of-Graphs/