這篇文章我們簡單的介紹求解圖的割點、割邊和二分圖相關的概念。
割點:
對於含n個點、m條邊的連通無向圖G,如果去掉頂點vi(並同時去掉與之相連的邊),使得G不再連通,那么稱vi是一個割點。
通過其定義,我們不難判斷某個點是否是割點,但是現在我們面臨的問題是,如何給出一個圖G,編碼讓計算機求解割點呢?
首先我們考慮這樣一個問題,判定某個點的指標是什么。我們通過人腦來判斷其是否是割點,其實是利用非常模糊的視覺效應,即“通過去掉該點觀察圖是否連通”即可,而如果想要通過計算機來判斷,就需要非常量化的判斷條件。
我們考慮從深度優先搜索的角度來找到這樣一個判斷條件,利用dfs遍歷圖,得到的生成子圖本質上會得到一個生成樹,我們拿出兩個相鄰的點vi、vj,vi是vj的父節點。我們回到深搜遍歷的過程中,假設當前遍歷到vj,如果我們從vj能夠找到一條回到已經訪問過的v1、v2...等節點,那么這表明去掉vi,將不會影響剩余圖的連通性。
我們似乎發現了些什么,但是這種判定關系還是有些模糊。
我們借用這樣一個概念——時間戳,即深度優先搜索的過程中,我們記錄訪問節點的順序,我們用num[i]來表示節點vi的時間戳,即在深搜遍歷過程中第幾個訪問vi節點。借用這個工具,我們考慮能不能將上述我們描述的關系用量化的表達式表示出來呢?好像還是有點捉襟見肘啊,我們不妨再設置一個數組low[i],用以表達vi不經過dfs的生成樹的父節點所能夠到達的時間戳最小的節點(好好理解,非常拗口),基於這個工具,我們能夠看到上述的判斷條件,可以用這樣一個表達式簡潔的概括:
low[j] < num[i]
那么現在我們首要的問題似乎變成了求解n個節點的low[]、num[]了。
首先,對於num[],也就是時間戳的記錄,並不困難。而對於low[]數組的求解,就需要動一些腦筋了。我們模擬遍歷過程,當前遍歷到vi點,我們訪問所有與vi連通的點vj,會出現如下兩種情況。
1.vj訪問過,被我們打上過時間戳, 那么我們此時需要更新low[i]了,即low[i] = min{num[j] | vj與vi連通}。
2.vj沒有訪問過,那么我們繼續深搜遍歷點的過程。
在遍歷完成之后,也完成了num[]、low[]的求解,我們再利用深搜的回溯過程,完成判斷即可。
這里需要注意的一點是,對於某個圖的根節點,即dfs開始的那個點(記作v1)其實是不滿足上文給出的判斷式子的,需要我們特殊判斷,記child是根節點的子樹個數,則v1是個割點的必要條件是,child = 2。
簡單的參考代碼如下。
#include<cstdio> #include<algorithm> using namespace std; int n , m , e[9][9] , root; int num[9] , low[9] , flag[9],index; int min(int a , int b) { return a < b ? a : b; } void dfs(int cur , int father) { int child = 0 , i , j; index++; num[cur] = index; low[cur] = index; for(i = 1;i <= n;i++) { if(e[cur][i] == 1) { if(num[i] == 0) //第一種情況 { child++; dfs(i,cur); low[cur] = min(low[cur] , low[i]);//回溯過程:判斷割點 if(cur != root && low[i] >= num[cur]) flag[cur] = 1; else if(cur == root && child == 2) flag[cur] = 1; } else if(i != father) //第二種情況 { low[cur] = min(low[cur] , num[i]); } } } } int main() { int i , j, x , y; scanf("%d %d",&n,&m); for(i = 1;i <= n;i++) for(j = 1;j <= n;j++) e[i][j] = 0; for(i = 1;i <= m;i++) { scanf("%d %d",&x,&y); e[x][y] = 1; e[y][x] = 1; } root = 1; dfs(1,root); for(i = 1;i <= n;i++) { if(flag[i] == 1) printf("%d ",i); } return 0; }
割邊:
有個割點的概念,割點非常好理解,即對於圖G,如果刪除邊ei,導致G的連通度發生變化,那么ei即是G的一個割邊。
那么我們來繼續思考如何利用編程實現求G的割邊。
基於上文我們對割點問題的思考,這里問題會顯得非常簡單,在判斷割點的時候,我們利用的核心判斷條件是low[j] >= num[i],其中vi是vj的父節點。那么拿到割邊上來,我們分兩部分看,如果low[j]>num[i],則表明去掉eij后,vj便不再和vj連通,這是符合割邊定義的。而如果low[j] = num[i],則表明去掉eij,vj依然能夠在不經過eij的情況下到達vi,連通度沒有發生改變。
因此我們可以看到,對於互相連通的父子節點vi、vj,滿足 low[j]>num[i],可判定eij是一條割邊。
基於dfs找割點的代碼,我們進行稍微的改動,有如下代碼。
#include<cstdio> #include<algorithm> using namespace std; int n , m , e[9][9] , root; int num[9] , low[9] , flag[9],index; int min(int a , int b) { return a < b ? a : b; } void dfs(int cur , int father) { int i , j; index++; num[cur] = index; low[cur] = index; for(i = 1;i <= n;i++) { if(e[cur][i] == 1) { if(num[i] == 0) //第一種情況 { dfs(i,cur); low[cur] = min(low[cur] , low[i]);//回溯過程:判斷割點 if(low[i] > num[cur]) printf("%d-%d\n",cur,i); } else if(i != father) //第二種情況 { low[cur] = min(low[cur] , num[i]); } } } } int main() { int i , j, x , y; scanf("%d %d",&n,&m); for(i = 1;i <= n;i++) for(j = 1;j <= n;j++) e[i][j] = 0; for(i = 1;i <= m;i++) { scanf("%d %d",&x,&y); e[x][y] = 1; e[y][x] = 1; } root = 1; dfs(1,root); return 0; }
二分圖的最大匹配:
我們先給出這樣導入模型的實際問題:現有n個妹子和n個漢字相約去坐過山車,過山車的結構是兩兩一排,現在要求坐一排的必須是一男一女,並且要求兩人必須相互認識,那么請問我們最多能安排多少對男女上過山車?
我們將問題抽象化,將每個個體視為點,而男女之間的是否認識視為點與點之間的邊,這里僅僅是強調了男生和女生的聯系,其余的關系我們不考慮,因此我們將男生放入A集合(該集合中的元素之間不存在邊),女生放入B集合,可以看到,這是典型的二分圖。
而我們將一對一對的男女送到兩座一排的過山車的過程,抽象得來看,可以用圖論中的術語——匹配,來表示。即將A中每個元素與B中的元素形成一一對應的關系,需要強調的是,只可以使一一對應的關系,而這種一一對應的匹配的對數,便稱作匹配數量。即高度概括一下我們即將導入的模型——如何求解二分圖的最大匹配數量。
我們注意到“最多”這個字眼,容易聯想到其與貪心算法有着密切的聯系,因此我們考慮從這個角度給出一個計算二分圖最大匹配數量的算法。
考慮將全局問題給子問題化然后尋求局部最優解,這樣方能引導出全局最優解。假設含2n個頂點的二分圖的一個分圖A含有n個頂點,我們依次遍歷集合A中的點v1、v2...vn,我們從過程開始分析,假設當前遍歷到vi點,顯然,我們盡可能的將vi匹配到B集合中的某個和vi相連的點,是當前局部最優的策略,那么我們不妨再遍歷B集合中與vi相連的點vi'、vj'......容易看到,對於這些點(以vi'為例),都會滿足如下的兩個性質之中的一個:
1.vi'在之前A集合(i-1)個點遍歷的過程中,並沒有與前i-1個點匹配。
2.vi'在之前A集合(i-1)個點遍歷的過程中,和前i-1個點中的某個點進行了匹配。
針對情況1,我們當然可以將vi與vi'匹配,則匹配數+1,這是當前的最優策略。
針對情況2,就顯得有些麻煩,既然vi'在A中已經有了匹配點,那么我們就此放棄vi了么?顯然不是,我們需要經過深思熟慮才能決定是否放棄vi。我們注意到初始情況的二分圖G是存在一對多的情況,即A集合中的某個點可能與B集合中的多個點均有邊,這其實就像是在匹配的時候留下了“其他選項 ”,其實就十分像我們利用dfs實現的回溯尋找迷宮,而我們在面對這種情況的時候,則需要利用dfs來回溯回去來嘗試所有這些“其他選項”,判斷在A集合前i-1個點在最大匹配數的基礎上,能否將vi返回情況1.如果存在,那則匹配數+1,這是當前的最優策略;如果不存在,那么即可放棄vi的匹配(想一想,為什么這里就可以直接放棄了,這種做法其實和前面的鋪墊是自洽的)。
理解了上文對求解二分圖最大匹配數算法的過程的描述,其正確性是不言自明的。概括起來,它本質上是一種貪心策略和基於dfs的窮舉策略。
簡單的參考代碼如下。
#include<cstdio> using namespace std; int e[101][101]; int match[101]; int book[101]; int n , m; int dfs(int u) { int i; for(i = 1;i <= n;i++) { if(book[i] == 0 && e[u][i] == 1) { book[i] = 1; if(match[i] == 0 || dfs(match[i])) { match[i] = u; match[u] = i; return 1; } } } return 0; } int main() { int i , j , t1 , t2 , sum = 0; scanf("%d %d",&n,&m); for(i = 1;i <= m;i++) { scanf("%d %d",&t1,&t2); e[t1][t2] = 1; e[t2][t1] = 1; } for(i = 1;i <= n;i++) match[i] = 0; for(i = 1;i <= n;i++) { for(j = 1;j <= n;j++) book[j] = 0; if(dfs(i)) sum++; } printf("%d",sum); return 0; }