二分圖系列•二分圖判定•匈牙利算法二分圖的最大匹配•二分圖最小點覆蓋及最大獨立集

 二分圖一•二分圖判定

描述

大家好,我是小Hi和小Ho的小夥伴Nettle,從這個星期開始由我來完成我們的Weekly。

新年回家,又到了一年一度大齡剩男剩女的相親時間。Nettle去姑姑家玩的時候看到了一張姑姑寫的相親情況表,上面都是姑姑介紹相親的剩男剩女們。每行有2個名字,表示這兩個人有一場相親。由於姑姑年齡比較大了記性不是太好,加上相親的人很多,所以姑姑一時也想不起來其中有些人的性別。因此她拜託我檢查一下相親表裏面有沒有錯誤的記錄,即是否把兩個同性安排了相親。

OK,讓我們愉快的暴力搜索吧!

纔怪咧。

對於拿到的相親情況表,我們不妨將其轉化成一個圖。將每一個人作爲一個點(編號1..N),若兩個人之間有一場相親,則在對應的點之間連接一條無向邊。(如下圖)

因爲相親總是在男女之間進行的,所以每一條邊的兩邊對應的人總是不同性別。假設表示男性的節點染成白色,女性的節點染色黑色。對於得到的無向圖來說,即每一條邊的兩端一定是一白一黑。如果存在一條邊兩端同爲白色或者黑色,則表示這一條邊所表示的記錄有誤。

由於我們並不知道每個人的性別,我們的問題就轉化爲判定是否存在一個合理的染色方案,使得我們所建立的無向圖滿足每一條邊兩端的頂點顏色都不相同

那麼,我們不妨將所有的點初始爲未染色的狀態。隨機選擇一個點,將其染成白色。再以它爲起點,將所有相鄰的點染成黑色。再以這些黑色的點爲起點,將所有與其相鄰未染色的點染成白色。不斷重複直到整個圖都染色完成。(如下圖)

在染色的過程中,我們應該怎樣發現錯誤的記錄呢?相信你一定發現了吧。對於一個已經染色的點,如果存在一個與它相鄰的已染色點和它的顏色相同,那麼就一定存在一條錯誤的記錄。(如上圖的4,5節點)

到此我們就得到了整個圖的算法:

  1. 選取一個未染色的點u進行染色
  2. 遍歷u的相鄰節點v:若v未染色,則染色成與u不同的顏色,並對v重複第2步;若v已經染色,如果 u和v顏色相同,判定不可行退出遍歷。
  3. 若所有節點均已染色,則判定可行。
#include<iostream>
#include<vector>
#include<cstring>
#include<queue>
using namespace std;

int N,M,book[10005];

void Check()
{
	queue<int> que;
	memset(book,0,sizeof(book));
	vector<int> node[N+2];      // vector[] 下標從0開始, 頂點從1開始編號,因此數組維數至少要比頂點總數大1
	int u,v;
	for(int i=1;i<=M;++i)
	{
		cin>>u>>v;
		node[u].push_back(v);
		node[v].push_back(u);
	}
	for(int k=1;k<=N;++k)
	{
		int flag=1;
		if(book[k]==0)
		{
			book[k]=1;
			que.push(k);
			while(!que.empty())     // Floodfill 把相鄰的頂點染成相反的顏色,1->-1 / -1->1
			{
				int tmp=que.front();//  cout<<"("<<tmp<<","<<book[tmp]<<")"<<endl;
				que.pop();
				for(vector<int>::iterator iter=node[tmp].begin();iter!=node[tmp].end();++iter)
				{
					if(book[tmp]!=book[(*iter)])
					{
						if(book[(*iter)]==0)
						{
							book[(*iter)]=-book[tmp];
							que.push((*iter));
						}
					}
					else
					{
						cout<<"Wrong"<<endl;
						return ;
					}
				}
			}
		}
	}
	cout<<"Correct"<<endl;
	return ;
}

int main()
{
	ios::sync_with_stdio(false);
	int T;
	cin>>T;
	while(T>0)
	{
		T--;
		cin>>N>>M;
		Check();
	}
	return 0;
}

二分圖二•二分圖最大匹配之匈牙利算法

描述

上一回我們已經將所有有問題的相親情況表剔除了,那麼接下來要做的就是安排相親了。因爲過年時間並不是很長,所以姑姑希望能夠儘可能在一天安排比較多的相親。由於一個人同一天只能和一個人相親,所以要從當前的相親情況表裏選擇儘可能多的組合,且每個人不會出現兩次。不知道有沒有什麼好辦法,對於當前給定的相親情況表,能夠算出最多能同時安排多少組相親呢?

同樣的,我們先將給定的情況錶轉換成圖G=(V,E)。在上一回中我們已經知道這個圖可以被染成黑白兩色。不妨將所有表示女性的節點記爲點集A,表示男性的節點記爲點集B。則有A∪B=V。由問題可知所有邊e的兩個端點分別屬於AB兩個集合。則可以表示成如下的圖:

同樣的,我們將所有的邊分爲兩個集合。集合S和集合M,同樣有S∪M=E。邊集S表示在這一輪相親會中將要進行的相親,邊集M表示在不在這一次進行。對於任意邊(u,v) ∈ S,我們稱u和v爲一組匹配,它們之間相互匹配。在圖G,我們將邊集S用實線表示,邊集M用虛線表示。得到下圖:

則原問題轉化爲,最多能選擇多少條邊到集合S,使得S集合中任何兩條邊不相鄰(即有共同的頂點)。顯然的,|S|<=Min{|A|, |B|}。

那麼能不能找到一個算法,使得能夠很容易計算出儘可能多的邊能夠放入集合S?我們不妨來看一個例子:

對於已經匹配的點我們先不考慮,我們從未匹配的點來做。這裏我們選擇A集合中尚未匹配的點(A3和A4)考慮:

對於A3點,我們可以發現A3與B4右邊相連,且都未匹配。則直接將(A3,B4)邊加入集合S即可。

對於A4點,我們發現和A4相連的B3,B4點都已經匹配了。但是再觀察可以發現,如果我們將A2和B2相連,則可以將B3點空出來。那麼就可以同時將(A2,B2),(A4,B3)相連。將原來的一個匹配變成了兩個匹配。

讓我們來仔細看看這一步:我們將這次變換中相關聯的邊標記出來,如下圖所示紫色的3條邊(A2,B2),(A2,B3),(A4,B3)。

這三條邊構成了一條路徑,可以發現這條路徑有個非常特殊的性質。虛線和實線相互交錯,並且起點和終點都是尚未匹配的點,且屬於兩個不同的集合。我們稱這樣的路徑爲交錯路徑。

再進一步分析,對於任意一條交錯路徑,虛線的數量一定比實線的數量多1。我們將虛線和實線交換一下,就變成了下面的圖:

在原來1個匹配的基礎上,我們得到了2個新的匹配,S集合邊的數量也增加了1。並且原來在已經匹配的點仍然是已經匹配的狀態。

再回頭看看A3點匹配時的情況:對於(A3,B4)這一條路徑,同樣滿足了交錯路徑的性質。

至此我們得到了一個找新匹配的有效算法:

選取一個未匹配的點,查找是否存在一條以它爲起點的交錯路徑。若存在,將該交錯路徑的邊虛實交換。否則在當前的情況下,該點找不到可以匹配的點。

又有對於已經匹配的點,該算法並不會改變一個點的匹配狀態。所以當我們對所有未匹配的點都計算過後,仍然沒有交錯路徑,則不可能找到更多的匹配。此時S集合中的邊數即爲最大邊數,我們稱爲最大匹配數。

那麼我們再一次梳理整個算法:

1. 依次枚舉每一個點i; 
2. 若點i尚未匹配,則以此點爲起點查詢一次交錯路徑。

最後即可得到最大匹配數。

在這個基礎上仍然有兩個可以優化的地方:

1.對於點的枚舉:當我們枚舉了所有A中的點後,無需再枚舉B中的點,就已經得到了最大匹配。
2.在查詢交錯路徑的過程中,有可能出現Ai與Bj直接相連,其中Bj爲已經匹配的點,且Bj之後找不到交錯路徑。之後又通過Ai查找到了一條交錯路徑{Ai,Bx,Ay,…,Az,Bj}延伸到Bj。由於之前已經計算過Bj沒有交錯路徑,若此時再計算一次就有了額外的冗餘。所以我們需要枚舉每個Ai時記錄B集合中的點是否已經查詢過,起點不同時需要清空記錄。使用book[i]標記尋找增廣路的過程中,頂點i是否已經查詢過即可避免不必要的過程。

僞代碼:
Function FindPath(u)
    For v∈u的相鄰節點
      If v沒有被標記過已經查詢
          標記v已經查詢過
          If v未匹配 or FindPath(v的匹配的點) Then
             更改u的匹配爲v
              Return Ture
          End If
    End For
Return False

/*主調函數*/
For i ∈ V
	清空查詢標記
    If  FindPath(i)  Then
        匹配計數+1
	End If

#include<iostream>
#include<cstring>
using namespace std;

int e[1005][1005],match[1005],book[1005],N,M;

bool DFS(int pos)
{
	book[pos]=1;
	for(int i=1;i<=N;++i)
	{
		if(e[pos][i]&&!book[i])
		{
			book[i]=1;
			if(match[i]==0||DFS(match[i]))  // 如果i已經被匹配了,則看與i匹配的另一頂點match[i]能否再找到匹配
			{
				match[pos]=i;    // i沒有匹配或者與i的匹配的另一頂點已找到其它新的匹配,則增廣路成功,登記信息
				match[i]=pos;
				return true;
			}
		}
	}
	return false;
}

int main()
{
	ios::sync_with_stdio(false);
	cin>>N>>M;
	for(int i=1;i<=M;++i)
	{
		int u,v;
		cin>>u>>v;
		e[u][v]=e[v][u]=1;
	}
	int tot=0;
	for(int t=1;t<=N;++t)
	{
		if(match[t]==0)
		{
			memset(book,0,sizeof(book));
			if(DFS(t))  tot++;
		}
	}
	cout<<tot<<endl;
	return 0;
}

二分圖三·二分圖最小點覆蓋和最大獨立集

描述

在上次安排完相親之後又過了挺長時間,大家好像都差不多見過面了。不過相親這個事不是說那麼容易的,所以Nettle的姑姑打算收集一下之前的情況並再安排一次相親。所以現在擺在Nettle面前的有2個問題:

1.姑姑想要了解之前所有相親的情況。對於任一個一次相親,只要跟參與相親的兩人交流就可以得到這次相親的情況。如果一個人參加了多次相親,那麼跟他交流就可以知道這幾次相親的情況。那麼問題來了,挖掘技術到底哪家強姑姑最少需要跟多少人進行交流可以瞭解到所有相親的情況。(二分圖最小點覆蓋->最少的點覆蓋所有邊)

問題1解答

2.因爲春節快要結束了,姑姑打算給這些人再安排一次集體相親。集體相親也就是所有人在一起相親,不再安排一對一對的進行相親。但是姑姑有個條件,要求所有參與相親的人之前都沒有見過。也就是說在之前的每一次相親中的兩人不會被同時邀請來參加這次集體相親。那麼問題又來了,姑姑最多可以讓多少人蔘與這個集體相親。

問題2解答

問題1.二分圖最小點覆蓋問題

同樣的轉化爲圖G=(V,E),則問題轉化爲:
圖G中選取儘可能少的點,使得圖中每一條邊至少有一個端點被選中。
這個問題在二分圖問題中被稱爲最小點覆蓋問題。即用最少的點去覆蓋所有的邊。
結論:由König定理可知最小點覆蓋的點數 = 二分圖最大匹配

König定理,二分圖最小點覆蓋 = 二分圖最大匹配數

二分圖最大匹配的König定理及其證明

    本文將是這一系列裏最短的一篇,因爲我只打算把König定理證了,其它的廢話一概沒有。
    以下五個問題我可能會在以後的文章裏說,如果你現在很想知道的話,網上去找找答案:
    1. 什麼是二分圖;
    2. 什麼是二分圖的匹配;
    3. 什麼是匈牙利算法;
    4. König定理證到了有什麼用;
    5. 爲什麼o上面有兩個點。

       König定理是一個二分圖中很重要的定理,它的意思是,一個二分圖中的最大匹配數等於這個圖中的最小點覆蓋數。如果你還不知道什麼是最小點覆蓋,我也在這裏說一下:假如選了一個點就相當於覆蓋了以它爲端點的所有邊,你需要選擇最少的點來覆蓋所有的邊。比如,下面這個圖中的最大匹配和最小點覆蓋已分別用藍色和紅色標註。它們都等於3。這個定理相信大多數人都知道,但是網絡上給出的證明並不多見。有一些網上常見的“證明”明顯是錯誤的。因此,我在這裏寫一下這個定理的證明,希望對大家有所幫助。

       假如我們已經通過匈牙利算法求出了最大匹配(假設它等於M),下面給出的方法可以告訴我們,選哪M個點可以覆蓋所有的邊。
       匈牙利算法需要我們從右邊的某個沒有匹配的點,走出一條使得“一條沒被匹配、一條已經匹配過,再下一條又沒匹配這樣交替地出現”的路(交錯軌,增廣路)。但是,現在我們已經找到了最大匹配,已經不存在這樣的路了。換句話說,我們能尋找到很多可能的增廣路,但最後都以找不到“終點是還沒有匹配過的點”而失敗。我們給所有這樣的點打上記號:從右邊的所有沒有匹配過的點出發,按照增廣路的“交替出現”的要求可以走到的所有點(最後走出的路徑是很多條不完整的增廣路那麼這些點組成了最小覆蓋點集:右邊所有沒有打上記號的點,加上左邊已經有記號的點。看圖,右圖中展示了兩條這樣的路徑,標記了一共6個點(用 “√”表示)。那麼,用紅色圈起來的三個點就是我們的最小覆蓋點集。
       首先,爲什麼這樣得到的點集點的個數恰好有M個呢?答案很簡單,因爲每個點都是某個匹配邊的其中一個端點。如果右邊的哪個點是沒有匹配過的,那麼它早就當成起點(獨立集尋找的起點)被標記了;如果左邊的哪個點是沒有匹配過的,那就走不到它那裏去(否則就找到了一條完整的增廣路)。而一個匹配邊又不可能左端點是標記了的,同時右端點是沒標記的(不然的話右邊的點就可以經過這條邊到達了)。因此,最後我們圈起來的點與匹配邊一一對應。
       其次,爲什麼這樣得到的點集可以覆蓋所有的邊呢?答案同樣簡單。不可能存在某一條邊,它的左端點是沒有標記的,而右端點是有標記的原因如下:如果這條邊不屬於我們的匹配邊,那麼左端點就可以通過這條邊到達(從而得到標記);如果這條邊屬於我們的匹配邊,那麼右端點不可能是一條路徑的起點,於是它的標記只能是從這條邊的左端點過來的(想想匹配的定義),左端點就應該有標記
       最後,爲什麼這是最小的點覆蓋集呢?這當然是最小的,不可能有比M還小的點覆蓋集了,因爲要覆蓋這M條匹配邊至少就需要M個點(再次回到匹配的定義)。

問題2.二分圖最大獨立集

依舊轉化爲圖G=(V,E),則問題轉化爲:
圖G中選取儘可能多的點,使得任意兩個點之間沒有連邊
這個問題在二分圖問題中被稱爲最大獨立集問題。
結論:最大獨立集的點數 = 總點數 - 二分圖最大匹配,(實際上出去最小獨立集頂點,剩下就是一個最大匹配)

證明:

假設最大獨立集的點數爲|U|,二分圖最大匹配的匹配數爲|M|,最大匹配中所有頂點集合爲EM
先證明 |U|≤|V|-|M|
M中任意一條邊的兩個端點是連接的,所有對於M中的邊必有一個端點不在|U|集合中,所以|M|≤|V|-|U|
再證明|U|≥|V|-|M|
首先我們知道一定有|U|≥|V|-|EM|,即將最大匹配的點刪除之後,剩下的點一定都不相連。
接下來我們考慮能否將M集合中的一個端點放入U中:
假設(x,y)屬於M,存在(a,x),(b,y),且a,b都在U中,則會出現兩種情況:

如果(a,b)連接,則有一個更大的匹配存在,矛盾 如果(a,b)不連接,a->x->y->b有一個新的增廣路,因此有一個更大的匹配,矛盾
故有a,b兩點中至多隻有1個點屬於U,則我們總是可以選取x,y中一個點放入集合U,所以|U|≥|V|-|EM|+|M|=|V|-|M|

綜上有|U|=|V|-|M|


發佈了194 篇原創文章 · 獲贊 457 · 訪問量 102萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章