首先來了解下一些概念性的東西。
二分圖:
二分圖又稱作二部圖,是圖論中的一種特殊模型。 設G=(V,E)是一個無向圖,如果頂點V可分割為兩個互不相交的子集(A,B),並且圖中的每條邊(i,j)所關聯的兩個頂點i和j分別屬於這兩個不同的頂點集(i in A,j in B),則稱圖G為一個二分圖。圖一就是一個二分圖。

匈牙利算法:
匈牙利算法是由匈牙利數學家Edmonds於1965年提出,因而得名。匈牙利算法是基於Hall定理中充分性證明的思想,它是一種用增廣路徑求二分圖最大匹配的算法。
Hall定理:
二部圖G中的兩部分頂點組成的集合分別為X, Y; X={X1, X2, X3,X4, .........,Xm}, Y={y1, y2, y3, y4 , .........,yn}, G中有一組無公共點的邊,一端恰好為組成X的點的充分必要條件是:X中的任意k個點至少與Y中的k個點相鄰。(1≤k≤m)
匹配:
給定一個二分圖G,在G的一個子圖M中,M的邊集中的任意兩條邊都不依附於同一個頂點,則稱M是一個匹配。圖一中紅線為就是一組匹配。
未蓋點:
設Vi是圖G的一個頂點,如果Vi 不與任意一條屬於匹配M的邊相關聯,就稱Vi 是一個未蓋點。如圖一中的a3、b1。
交錯路:
設P是圖G的一條路,如果P的任意兩條相鄰的邊一定是一條屬於M而另一條不屬於M,就稱P是一條交錯路。如圖一中a2->b2->a1->b4。
可增廣路:
兩個端點都是未蓋點的交錯路叫做可增廣路。如圖一中的b1->a2->b2->a1->b4->a3。
頂點的數目:
圖中頂點的總數。
最大獨立數:
從V個頂點中選出k個頂,使得這k個頂互不相鄰。 那么最大的k就是這個圖的最大獨立數。
最小頂點覆蓋數:
用最少的頂點數k來覆蓋圖的所有的邊,k就是這個圖的最小頂點覆蓋數。
最大匹配數:
所有匹配中包含的邊數最多的數目稱為最大匹配數。
頂點的數目=最大獨立數+最小頂點覆蓋數(對於所有無向圖都有效)
最大匹配數=最小頂點覆蓋數(只對二分圖有效)
這么多概念羅列出來了,下面放個實際中可能遇到的問題。其實就是算法競賽中的一道題目,簡化闡述下。
問題:
分析:
可以按男女畫出二分圖。因為要求選出的小孩都要相互認識,則可以在相互不認識的小孩之間連線,這樣只要小孩之間沒有線直接相連,那么他們就肯定就是相互認識的了。也就因此轉化為了求這個二分圖的最大獨立數。
為了便於理解,首先我們來做個游戲。假設現在有5個男孩(b1,b2,b3,b4,b5)、五個女孩(g1,g2,g3,g4,g5)。b1不認識g1,g2;b2不認識g2,g3;b3不認識g2,g5;b4不認識g3;b5不認識g3,g4,g5。男孩女孩站兩排,相互不認識的他們之間用一根線相連。得到的圖如下:

因為相互不認識的小孩之間會有一根線,所以如果我們想得到相互都認識的小孩,那么最終留下的小孩他們的手里都不能握有線了,很簡單如果他們手中還有線,那就說明留下的人還有他不認識的。這樣,之前的問題也就轉變為了如何去除最少的小孩,使留下的小孩手中沒有線。再換一種說法,也就是怎么在這么多小孩中找出最少的人,他們握有所有的線。這下就轉換為了尋找最小頂點覆蓋數。
這樣如果找到了最小頂點覆蓋數,我們又知道頂點數(所有的小孩的數目),就可以求出最大獨立數(現場留下的小孩)。
又因為對於二分圖,最大匹配數=最小頂點覆蓋數。這個問題進而也就變成了求解最大匹配數。而匈牙利算法正是用來求最大匹配數的一個很好的方法。
下面我們就來看看匈牙利算法的具體流程。

上面的流程有些抽象,具體要怎么來找增廣路呢,下面給出具體操作的流程圖。

是不是感覺有些亂,讓我們來一步一步的分析。為了簡化步驟我們用下圖來分析

第一個最外層的循環從x1開始:
按照流程圖,第一次肯定有xi了,清空標記后,yi肯定也是沒有標記的了。然后對yi進行標記,第一次M自然也是空的了,M中加入(x1,y1)得到新的M{(x1,y1)},於是得到下圖。

最外層循環到了x2:
好,接下來最大匹配數加1,循環繼續。下面就該輪到x2了。首先清空Y的標記。

這時x2再找到y1時它已經沒有標記了,這時再來標記它。但y1已經在M中了,它的對應頂點為x1。所以接下來x1要更新關聯點。

由x1開始查找時,在Y中y1已經被標記了,只能找下一個頂點,於是便找到了y2。y2未被標記,標記它。x1的關聯點更新為y2,x2的關聯點更新為y1。因此得新的M為{(x2,y1),(x1,y2)}


好,最大匹配數加1,循環繼續。清空Y中標記。
最外層循環到了x3:

下面就輪到x3了。x3找到y1,y1未標記,標記之。

y1的關聯是x2。又輪到x2找了。x2找到y1,但y1已被標記,於是便找到y2。y2未被標記,標記之。

y2的關聯為x1,x1開始找。x1找到y1,y1已被標記,找到y2,y2已被標記。找到y3,y3未被標記,標記之。同時y3沒有關聯。更改x1的關聯為y3。


原(x1,y2)的關聯也因為x1的改變,變為了(x2,y2)


同時新增關聯(x3,y1)更新M為{(x3,y1),(x2,y2),(x1,y3)}。最大匹配數加1。這時已經沒有更多的X中頂點可選了。最大匹配數就這樣被找出來了。

細心的人可能已經看出來了,有M'{(x2,y1),(x1,y2)},最后找出的路徑x3->y1->x2->y2->x1->y3是一條增廣路徑。

下面說一些增廣路的特性,匈牙利法的正確性驗證自己有興趣可以證明下。
(1)有奇數條邊。
(2)起點在二分圖的左半邊,終點在右半邊。
(3)路徑上的點一定是一個在左半邊,一個在右半邊,交替出現。
(4)整條路徑上沒有重復的點。
(5)路徑上的所有第奇數條邊都不在原匹配中,所有第偶數條邊都出現在原匹配中。
(6)把增廣路徑上的所有第奇數條邊加入到原匹配中去,並把增廣路徑中的所有第偶數條邊從原匹配中刪除(這個操作稱為增廣路徑的取反),則新的匹配數就比原匹配數增加了1個。
接下來就要用計算機來實現這個算法的過程了。相信有了上面的基礎,已經不難完成了。
我是用c++編譯的,程序很多地方肯定還有待優化,主要是為了展示算法流程。到這里至少應該對匈牙利算法有所了解了,算法講解到此結束。
#include <stdio.h> #include <string.h> #define MAXNUM 1000 //遞歸 //xi 二分圖左部中的頂點 //ytotal 二分圖右部頂點總數 //relation xy之間的關聯關系 //link xy之間的匹配 //y的標記 bool recursion(const int xi, const int ytotal, const bool relation[][MAXNUM], int link[], bool* sign) { for(int i = 0; i < ytotal; i++) { if(relation[xi][i] && !sign[i])//有關聯並且沒被標記 { sign[i] = true;//標記 if(link[i] == -1 || recursion(link[i], ytotal, relation, link, sign))//y沒有有匹配則更新y的匹配;y有匹配則用它的匹配繼續查找 { link[i] = xi;//更新y的匹配 return true; } } } return false; } //匈牙利算法 //xtotal 二分圖的左部包含頂點總數 //ytotal 二分圖的右部包含頂點總數 //xy之間的關聯 int Hungary(const int xtotal, const int ytotal,const bool relation[][MAXNUM]) { int link[MAXNUM][2];//與y匹配的x memset(link, -1, sizeof(link)); int cnt = 0;//最大匹配數 for(int i = 0; i < xtotal; i++) { bool sign[MAXNUM] = {false};//清空標記 if(recursion(i, ytotal, relation, link[i], sign))//尋找增廣路徑 { cnt++; } } return cnt; } int main(int argc, char** argv) { while(true) { int boytotal = 0; int girltotal = 0; bool relation[MAXNUM][MAXNUM] = {false}; //獲取男孩總數 do { fflush(stdin); printf("請輸入男孩總數(不大於%d):\n", MAXNUM); scanf("%d",&boytotal); }while(0 == boytotal || boytotal > MAXNUM); printf("男孩總數輸入成功。\n"); printf("------------------------\n"); //獲取女孩總數 do { fflush(stdin); printf("請輸入女孩總數(不大於%d):\n", MAXNUM); scanf("%d",&girltotal); }while(0 == girltotal || girltotal > MAXNUM); printf("女孩總數輸入成功。\n"); printf("------------------------\n"); //獲取相互不認識的男女 do { int boyno = 0; int girlno = 0; fflush(stdin); printf("請輸入相互不認識的異性小孩,如第一個男孩不認識第二個女孩則輸入1,2:\n"); scanf("%d,%d",&boyno, &girlno); if(boyno>0&&boyno<=boytotal&&girlno>0&&girlno<=girltotal) { relation[boyno-1][girlno-1] = true; char ret = '\0'; fflush(stdin); printf("一對互不認識的男女輸入成功,是否結束輸入?(Y/N)\n"); scanf("%c",&ret); if('Y' == ret || 'y' == ret) { break; } } }while(true); printf("------------------------\n"); printf("男孩個數:%d;女孩個數:%d;小孩總數:%d\n",boytotal,girltotal,boytotal+girltotal); //尋找最大匹配 int tmp = Hungary(boytotal, girltotal, relation); printf("最大匹配數:%d\n", tmp); printf("最多可以留下人數:%d\n", boytotal+girltotal-tmp); printf("------------------------\n"); char ret; fflush(stdin); printf("是否退出?(Y/N)\n"); scanf("%c",&ret); if('Y' == ret || 'y' == ret) { break; } printf("------------------------\n"); } return 1; }
