(演算法競賽)簡單易懂二分圖

夏月冬雪發表於2020-08-15

今天本來想整理\(Kruskal\)演算法和次小生成樹的求解方法的,但是介於被一個求最大獨立集的gou題卡了將近\(5\)個小時,所以決定先整理一下二分圖的知識

FBI Warning:本篇部落格極其哲學,不喜勿噴謝謝您的配合

Part 1:二分圖的定義及判定

給出定義:

二分圖又稱作二部圖,是圖論中的一種特殊模型。 設\(G=(V,E)\)是一個無向圖,如果頂點\(V\)可分割為兩個互不相交的子集\((A,B)\),並且圖中的每條邊\((i,j)\)所關聯的兩個頂點\(i\)\(j\)分別屬於這兩個不同的頂點集\((i \in A,j \in B)\),則稱圖\(G\)為一個二分圖。
——來自百度百科

百度百科的解釋依舊很詭異,我們用圖+文字來具體的描述一下(靈魂畫師請戰):

現在給出這樣的一張\(4\)個點\(4\)條邊的無向圖:

我們交換\(B\)\(b\)的位置,重新把這個圖畫一遍:

可以發現,這個圖現在被我們劃分成了左邊(\(A、B\))和右邊(\(a、b\))兩部分

而且這兩部分滿足這樣的性質:同屬左邊或右邊的點之間沒有邊相連

只要一個無向圖被劃分成左右兩部分之後,可以滿足上述條件,那麼這張圖就是一個二分圖

現在給出更加一般且方便的二分圖判定法則:

定理:假設每條邊的長度為\(1\),如果一個無向圖中沒有長度為奇數的環,那麼這個圖就是一張二分圖

警告:以下的證明作者幾乎都在扯\(dan\),如果您想更快的學習二分圖的知識,請直接跳過證明部分

我們來嘗試感性的證明一下這個定理

開始證明

用黑白染色法來區分開左右的點,一開始,所有的點都沒有被染色(設左邊的點都是黑色,右邊的點都是白色)
還是\((1\rightarrow 2\rightarrow 3\rightarrow 1)\)的奇環,不妨設\(1\)是黑色,如果這是一張二分圖,那麼\(2\)\(1\)相連,\(2\)應該為白色,\(2\)\(3\)相連,\(3\)應該為黑色,\(1\)\(3\)相連,\(1\)應該是白色,這顯然與一開始的假設(\(1\)是黑色)矛盾了,所以“這是一張二分圖”的假設不成立,故這不是一張二分圖
那麼現在,我們把長度為\(3\)這個假設去掉,改成存在長度為\(x\)\(x\)%\(2=1\))的環
我們先把\(1\)號點和\(x\)號點之間的邊斷開,單純的研究這一條鏈:
不妨設\(1\)號點是黑色,如果想要滿足二分圖的性質,相鄰點的顏色必須不同,那麼\(2\)是白色,以此類推:\(3\)黑,\(4\)白,\(5\)黑……
繼而很簡單的可以推出:編號為奇數的點一定都是黑色,編號為偶數的點一定都是白色,那麼\(x\)號點一定是黑色(因為\(x\)是奇數)
但是不要忘了,\(x\)號點和\(1\)號點有一條邊相連,此時\(x\)號點和\(1\)號點都是黑色且有邊相連,顯然不滿足二分圖的性質,所以如果存在奇數環的圖,一定不是二分圖
那麼對於偶環呢?顯然偶環的最後一個點和第一個點的顏色不同,所以一張圖存不存在偶環對於這張圖是不是二分圖並沒有影響
對於鏈就更沒有影響了:只要一個放左邊一個放右邊一直放下去直到放完整條鏈就\(OK\)
眾所周知,一張圖一定是由環和鏈構成的,然後我們把這三種情況全都討論了一遍,發現一張圖是不是二分圖僅與這張圖中有沒有奇環有關
那麼證畢:當且僅當一張圖中沒有奇環時,無向圖是一張二分圖
以上證明全部是作者\(yy\)上去的,沒有任何科學依據,僅供方便讀者理解!!!

證畢

Part 2:二分圖上的一些\(knowledge\)(定義)

現在我們來康康關於二分圖還有什麼其他刺激的知識:

\(1、\)最大匹配

定義:在二分圖\(G\)的一個子圖\(M\)中,\(M\)的邊集中的任意兩條邊都不依附於同一個頂點,則稱\(M\)是一個匹配。選擇這樣的邊數最大的子集稱為圖的最大匹配問題,最大匹配的邊數稱為最大匹配數

按照作者的尿性,我們要用通俗易懂的語言化簡一下複雜的定義,我們用一個妙趣橫生的故事,來幫助理解和記憶

今年是公元\(2050\)年,為了幫助更多的單身人士脫單,\(Z\)國決定組織一場全國性的相親大會,而你被當選為大會的主辦人

現在我們有\(n\)位男嘉賓(左邊)和\(m\)位女嘉賓(右邊),他們之間有\(k\)個互相中意的關係(邊)(這就是一張二分圖了)

但是\(Z\)國人非常的有追求和理想,當且僅當男女嘉賓互相中意時,他們才能結對(只有左邊節點和右邊節點右邊相連時,才能結對)

並且\(Z\)國的法律不允許“一夫多妻”或者“一妻多夫”,所以一位男嘉賓只能和一位女嘉賓匹配,反之亦然

國家主席布里jiojio鄧布利多先生對國家的生育率表示擔憂,所以他希望你能撮合出最多的情侶來

現在這個“最多的情侶數”就是“二分圖最大匹配”數了,如果把定義套到情境裡去,那麼它就變成了這樣:

使用\(k\)個關係,聯絡男生和女生,在不出現一個人和多個異性聯絡的情況下,使得聯絡對數最多。此時這個最多的對數,就是二分圖最大匹配數

\(2、\)最小點覆蓋

定義:最小點覆蓋是指用最少的頂點數使得二分圖\(G\)中的每條邊都至少與其中一個點相關聯

其實這個定義已經很好理解了,如果理解不了,那就又到了講故事時間

\(XX\)中學\(Z\)班中,出現了談戀愛的不良風氣!現在班主任布里jiojio鄧布利多先生已經掌握了情報,他發現這些談戀愛的學生可以組成這樣的關係網:

其中有\(n\)個男生(左邊\(n\)個點),\(m\)個女生(右邊\(m\)個點),\(k\)對戀愛關係(邊),並且讓人作嘔的是,有些同學與多個異性保持了戀愛關係!

現在布里jiojio鄧布利多先生希望把這些談戀愛的學生一網打盡,整頓班級風氣,但是因為還有別的班級事務要管理,他決定對於每一對情侶,僅找其中的一方談話教育

按道理來說,他需要談話\(\frac{k}{2}\)次,但是因為有些同學與多個異性保持了戀愛關係!,所以實際上他並不需要進行那麼多次談話

時間緊迫,他需要你幫助他求出,他最少要談話多少次才能保證這些情侶中的每一對中至少有一個人被談話

現在把定義引申到情境中,它是這樣的:

尋找一個點集\(S\)(談戀愛的同學),使得圖中去掉與點集\(S\)中的元素相連的邊(談話後破壞關係)之後,一條邊也不剩(肅清班風),目標是:最小化點集\(S\)包含的元素個數

\(3、\)最小邊覆蓋

定義:用盡量少的不相交簡單路徑覆蓋二分圖中的所有頂點

這個定義稍微有點難理解,所以是講故事時間

讓我們回到第一個情景:國家主席布里jiojio鄧布利多先生對於相親大會的成果並不滿意,在你向他說明情況後,他決定修改本國憲法

現在,\(Z\)國的憲法允許一夫多妻和一妻多夫制,現在,主席要求你幫所有人找到心儀的(有邊相連的才算“心儀”)物件

就是在相親大會上,當大部分人都得到了理想的匹配之後,總有一些人無法匹配(讓我們假設其中的一名男性同胞叫做小\(A\)

怎麼辦呢?小\(A\)並不想單身啊\(QAQ\)可是他喜歡的小姐姐已經有男朋友小\(B\)了!這時候你過來了,你讓小\(A\)與小\(B\)都當小姐姐的男朋友

於是問題圓滿地解決了,所有人都找到了心儀的物件(滑稽)

但是這還沒完,你發現,如果一夫多妻或一妻多夫的人數太多,會導致人事糾紛,所以你想讓這種關係的人數儘可能少

現在使得這種關係的人數最少的解,就是二分圖最小邊覆蓋問題的解,用自己的話說定義就是:

定義:在所有點都找到匹配(找到心儀物件)的前提下,使得一個點匹配多個點(一夫多妻或一妻多夫)的情況數最少

\(4、\)最大獨立集

定義:最大獨立集是指尋找一個包含元素最多的點集,使得其中任意兩點在圖中無對應邊

這個定義真的很好理解了,連故事都不用講(其實是作者想象力太差編不出來了)

就是選出最多的點,使得圖中沒有一條邊直接連線這些點中的任何兩個(我感覺反而說複雜了)

問題本質

上面叨叨了一大堆定義,難道我們對於每個定義都要設計一種演算法來求嗎?

顯 然 不 可 能 !

最小割定理:一個二分圖中的最大匹配數=最小點覆蓋數

沒錯上面叨叨的了一大頓的最小點覆蓋和最大匹配就是一個相同的問題!

定理證明:

首先,最小點覆蓋一定\(\geq\)最大匹配,因為假設最大匹配為\(n\),那麼我們就得到了\(n\)條互不相鄰的邊,光覆蓋這些邊就要用到\(n\)個點。
現在我們來思考為什麼最小點覆蓋一定\(\geq\)最大匹配。任何一種\(n\)個點的最小點覆蓋,一定可以轉化成一個\(n\)的最大匹配。
因為最小點覆蓋中的每個點都能找到至少一條只有一個端點在點集中的邊(如果找不到則說明該點所有的邊的另外一個端點都被覆蓋,所以該點則沒必要被覆蓋,和它在最小點集覆蓋中相矛盾)
只要每個端點都選擇一個這樣的邊,就必然能轉化為一個匹配數與點集覆蓋的點數相等的匹配方案。所以最大匹配至少為最小點集覆蓋數,即最小點覆蓋一定\(\geq\)最大匹配。綜上,二者相等。

證畢

現在來看看剩下的最小邊覆蓋和最大獨立集

最小邊覆蓋要求使得一個點匹配多個點的情況數最少,那麼請感性理解一下:

感性證明

我們從定義出發考慮:最小邊覆蓋是怎麼計算的?
當一個點沒有得到匹配,我們強行給它匹配上(上面那個小\(A\)的情景),記操作次數\(+1\),那麼最小邊覆蓋就是使得所有點都匹配上的前提下,最少的操作次數
那麼,既然只要一個點沒有得到匹配,就要強行操作一次,我們很容易就想到,只要使得沒有匹配上的人數最少不就行了嗎?
顯然這麼做,我們求出的最小邊覆蓋應該等於沒有匹配上的人數,要想使得沒有匹配上的人數最少,只需要讓匹配上的人數最多即可。
那麼怎麼求最多匹配呢?當然是二分圖最大匹配。得出結論:最小邊覆蓋=二分圖點數-最大匹配數

證畢

所以最小邊覆蓋實質上也是求最大匹配,再來看最大獨立集

最大獨立集要求求一個元素最多的點集,使得其中每兩個點之間沒有邊直接相連。

感性證明

我們可以這麼想:如果我把一個圖的邊全部幹掉該多好!那樣我的最大獨立集就是\(n\)了!
突然想到最小點覆蓋:尋找一個元素最少的點集\(S\),使得圖中去掉與點集\(S\)中的元素相連的邊之後,一條邊也不剩
去掉最少的點使得一條邊也不剩哎!去掉的最少意味著剩下的最多,一條邊也不剩意味著剩下的點兩兩之間沒有邊相連,就是最大獨立集的定義
所以得出結論:最大獨立集=點數-最小點覆蓋,最小點覆蓋=最大匹配,最大獨立集=\(n\)-最大匹配

證畢

所以對於上面所有的\(4\)種問題,其本質都可以轉化成求二分圖最大匹配的問題

但是我猜大家第一次看的時候,都會覺得這是\(4\)個互相獨立的問題,出題人更是會根據這個特性來出各種各樣的毒瘤題目,所以如果見到二分圖的題不要怕,弄清楚題目問的是這\(4\)個問題的哪一種,然後求出最大匹配即可!

Part 3:求解二分圖最大匹配問題

染色建圖

使用匈牙利(增廣路)演算法可以方便的求解最大匹配問題,但是在匈牙利(增廣路)演算法之前,得先學會判斷和建立二分圖。。。

前面我有提到過黑白染色法,我們判斷和建立二分圖都是使用這個方法

實現也很暴力,如果當前點是黑的,那麼與這個點相連的點一定是白的,反之亦然,然後遞迴染色即可

如果碰到矛盾的情況(比如說上面的那個奇環)說明這個圖不是二分圖

當然了,給出的圖不一定連通,所以我們要掃一遍所有點,如果當前點沒有被染色,那麼就以這個點為起點進行染色

\(Code\)

void paint(const int x,const int color){
	int y;
	col[x]=color;//表示第x個點的顏色
	for(unsigned int i=0;i<v[x].size();i++){
		if(col[y=v[x][i]]==-1){//沒有被染色 
			if(color==0) paint(y,1);//1表示白色 
			else paint(y,0);//0表示黑色 
		}
	}
}

匈牙利(增廣路)演算法

這裡我們拋開原理,只談實現和運用

工作過程:
\(1、\)設集合\(S\)是空集,即所有邊都是非匹配邊
\(2、\)尋找增廣路\(path\),把路徑上的所有邊的匹配狀態取反,得到一個更大的匹配\(S'\)
\(3、\)重複第\(2\)步,直到圖中不存在增廣路
這個演算法的關鍵在於如何找到一條增廣路。匈牙利演算法依次嘗試給每一個左部節點\(x\)尋找一個匹配的右部節點\(y\)
右部點\(y\)可以與左部點\(x\)匹配,一定滿足下面兩種情況的一種:
\(1、\)\(y\)本身就是非匹配點,此時無向邊\((x,y)\)本身就是非匹配邊,自己構成一條長度為\(1\)的增廣路
\(2、\)\(y\)已經與左部點\(x'\)匹配,但從\(x'\)出發能找到另一個\(y'\)\(x'\)匹配,此時路徑\(x~y~x'~y'\)為一條增廣路
——《演算法競賽進階指南》李煜東

但是顯然李煜東大佬給的這個解釋我看不懂,於是用一些奇怪的途徑瞭解了匈牙利演算法之後,我編了這樣一個故事幫助理解:

還是相親大會的情景,為了滿足布里jiojio鄧布利多主席的要求,你知道自己需要求出二分圖最大匹配數,但是可惜的是你不知道怎麼求

時間緊迫,你決定用自己的方法試一試

首先,你讓所有男嘉賓站在了一起,女嘉賓站在了一起,也就是這樣:(假設男左女右,黑線代表可以匹配,紅線表示已經匹配)

你從左邊的第一個點開始嘗試給他們配對,首先是\(0\)號男嘉賓

\(0\)號說:“我只喜歡\(2\)號小姐姐!”然後你帶著他去找\(2\)號,發現\(2\)號沒有男朋友,於是你先給他們匹配上了

現在你去找第二個男嘉賓\(3\)號,\(3\)號說:“我也喜歡\(2\)號小姐姐”然後你帶著他去找\(2\)號,發現\(2\)號已經有男朋友了,是剛才匹配的\(0\)

你又帶著\(3\)號去問\(0\)號:“你能不能換一個女朋友,然後把\(2\)號讓給\(3\)號?”

但是\(0\)號除了跟\(2\)號有連線,就沒有別的連線了,於是\(0\)號說:“不行我就只喜歡\(2\)號,我不幹!”

沒辦法,你又問\(3\)號:“除了\(2\)號你還有喜歡的小姐姐嗎?”\(3\)號說:“我覺得\(1\)號也挺好的”

於是你們去找\(1\)號,發現\(1\)號沒有男朋友,所以\(3\)號和\(1\)號順利匹配

現在是第三個男嘉賓\(4\)號,\(4\)號說:“我喜歡\(1\)號!”

於是你們去找\(1\)號,\(1\)號有男朋友是\(3\)號。還是老套路,你去問\(3\)號能不能換女朋友,把\(1\)號讓出來給\(4\)

\(3\)號比較大方(花心):“其實吧,\(5\)號人也挺好的。”然後\(3\)號去找\(5\)號,發現\(5\)號沒有男朋友,於是\(3\)號和\(5\)號匹配,把\(1\)號讓了出來

因為\(1\)號被\(3\)號讓了出來,所以\(4\)號和\(1\)號成功匹配

最後一個男嘉賓\(6\)號:“我喜歡\(1\)號!”於是你們去找\(1\)號,\(1\)號有男朋友是\(4\)

老套路,你去問\(4\)號能不能換,把\(1\)號讓出來給\(6\)號,可是\(4\)號除了和\(1\)號之外沒有邊了,他說:“不換不換!老子不換!說什麼也不換!”

然而\(6\)號也只喜歡\(1\)號,但是因為先到先得,你只能遺憾的告訴\(6\)號他得孤獨終老了

現在筆者告訴你,剛才你所完成的,就是匈牙利演算法的全過程了,這張二分圖的最大匹配數就是\(3\)

這裡寫程式碼的時候注意如果遇到更換的情況,新換的那個點不可以是需要讓出來的那個點

對於以上繁雜的過程,筆者用了一個更加違反人類倫理的話來總結:

對於單身的小姐姐,直接去追,對於已經有男朋友的小姐姐,去問問她男朋友能不能換一個小姐姐當女朋友,把這個小姐姐讓給自己

好啦不扯皮了,相信經過筆者的一通比喻,大家已經明白了匈牙利演算法的實現原理了,下面來看程式碼吧!

這裡假設我們已經對二分圖染好色並存在了\(col\)陣列裡

bool dfs(const int x){
//match[i]存右邊i號點的匹配是左邊的哪個點
	for(unsigned int i=0;i<v[x].size();i++){//掃描x的所有出邊
		int y=v[x][i];
		if(vis[y]==0){//如果掃描到的這個點不是需要讓出來的那個點
			vis[y]=1;//標記y點不能被需要更換的情況作為替換點
			if(match[y]==-1||dfs(match[y])){ //match[y]==-1說明這個右邊點沒有匹配過,dfs(match[y])如果為真說明可以換
				match[y]=x;//現在y和x匹配
				return true;//返回真
			}
		}
	}	
	return false;//如果不滿足上面那一大串的條件,說明換不了,返回假
}
inline int APath(){//嘗試匹配
	int res=0;
	for(int u=0;u<n;u++){//對於所有的點,我們只需要對黑點或白點嘗試匹配即可
		memset(vis,0,sizeof(vis));
		if(col[u]==0){//如果是黑點就嘗試匹配 
			if(dfs(u)) res++;//匹配成功 
		}
	}
	return res;//返回值為最大匹配數
}

Part 4:例題

學了這麼牛B(哲學)的演算法,怎麼能不拿出來練練手呢?

傳送門:https://www.luogu.com.cn/problem/P6268

題目中給出了\(m\)條邊,被邊連線的兩個點種只能選一個,求最多選幾個點

很容易就看出來本題是求最大獨立集的一道題目,由於題目中給出的只是邊連線的兩個點,並不知道左還是右,所以我們要先染色

染好色後(本題的資料保證是二分圖了,所以不用特判)我們直接跑一遍匈牙利,然後\(n-\)最大匹配數即可

然而交上去只有\(60\)分,不知所措的我\(De\)了一下午\(bug\),然並卵(不讓下資料蛋疼),最後憤怒的我抄了個題解下來去對拍

發現:我在記錄右部節點的匹配點時,預設\(match[y]==0\)是沒有匹配,但是,\(TND\)這個題有個\(0\)號點,如果\(0\)號點已經和\(y\)匹配上了,查詢的時候還是預設\(y\)沒有匹配過,導致找到了錯的增廣路,然後發現匹配數比真實的答案要大\(1\),然後\(WA\)掉了……

所以為了避免類似的慘劇發生,這裡強烈建議大家把\(match\)初始化成\(-1\),避免踩坑

\(Code\)

#include<cstdio>
#include<cstring>
#include<queue>
#include<stack>
#include<algorithm>
#include<set>
#include<map>
#include<utility>
#include<iostream>
#include<list>
#include<ctime>
#include<cmath>
#include<cstdlib>
#include<iomanip>
typedef long long int ll;
inline int read(){
	int fh=1,x=0;
	char ch=getchar();
	while(ch<'0'||ch>'9'){ if(ch=='-') fh=-1;ch=getchar(); }
	while('0'<=ch&&ch<='9'){ x=(x<<3)+(x<<1)+ch-'0';ch=getchar(); }
	return fh*x;
}
inline int _abs(const int x){ return x>=0?x:-x; }
inline int _max(const int x,const int y){ return x>=y?x:y; }
inline int _min(const int x,const int y){ return x<=y?x:y; }
inline int _gcd(const int x,const int y){ return y?_gcd(y,x%y):x; }
inline int _lcm(const int x,const int y){ return x*y/_gcd(x,y); }
const int maxn=5005;
std::vector<int>v[maxn];
int match[maxn],m,n,col[maxn];//col存每個點的顏色 
bool vis[maxn];
bool dfs(const int x){
	for(unsigned int i=0;i<v[x].size();i++){
		int y=v[x][i];
		if(vis[y]==0){
			vis[y]=1;
			if(match[y]==-1||dfs(match[y])){ 
				match[y]=x;
				return true;
			}
		}
	}	
	return false;
}
inline int APath(){//求增廣路 
	int res=0;
	for(int u=0;u<n;u++){
		memset(vis,0,sizeof(vis));
		if(col[u]==0){//如果是黑點就尋找增廣路 
			if(dfs(u))res++;//匹配成功 
		}
	}
	return res;
}
void paint(const int x,const int color){
	int y;
	col[x]=color;
	for(unsigned int i=0;i<v[x].size();i++){
		if(col[y=v[x][i]]==-1){//沒有被染色 
			if(color==0) paint(y,1);//1表示白色 
			else paint(y,0);//0表示黑色 
		}
	}
}
int main(){
//	freopen("data.in","r",stdin);
//	freopen("sol.out","w",stdout);
	memset(match,-1,sizeof(match));
	n=read(),m=read();
	for(int i=0,x,y;i<m;i++){
		x=read(),y=read();
		if(x==y) continue;
		v[x].push_back(y);
		v[y].push_back(x);
	}
	memset(col,-1,sizeof(col));//一開始都沒有被染色 
	for(int i=0;i<n;i++)
		if(col[i]==-1) paint(i,0);//如果沒有被染色,預設為黑色
	int path=APath();
	printf("%d\n",n-path);
	return 0;
}

好了今天關於二分圖和增廣路的分享就到這裡了,感謝您的閱讀,如果覺得筆者寫的不錯的話,請高抬貴手來個三連,謝謝您的支援!!!

相關文章