1. 程式人生 > >二分圖的基本概念+二分圖的最大匹配問題(匈牙利演算法)

二分圖的基本概念+二分圖的最大匹配問題(匈牙利演算法)

       今天學了二分圖的最大匹配,其中的匈牙利演算法。。哦不,其實遠不止這個,還有後面的一系列KM、開花樹啊什麼的演算法。反正又是一個異常懵逼的一天。。。

我覺得應該是上課前沒有稍微預習一下這個演算法是什麼,瞭解個大概,導致上課總是老師講到後面了,我卻還在糾結前面的內容。。可以當作經驗教訓。

       明天他們要去杭電打團體賽。。也不知道怎麼的,當時明明跟老師說了,卻沒報上名。。。這樣也好,最近太累了需要多休息,明天還可以把今天講的幾個演算法。自己好好深入研究下。程式設計嘛,無它,唯手熟爾偷笑下面就一步步來,先把最基礎的二分圖的最大匹配問題搞懂。。

       我先把二分圖的基本概念放這兒,改天深入剖析一下二分圖,再把概念挪過去

1.二分圖又稱作二部圖,是圖論中的一種特殊模型。 設G=(V,E)是一個無向圖,如果頂點V可分割為兩個互不相交的子集(A,B),並且圖中的每條邊(i,j)所關聯的兩個頂點i和j分別屬於這兩個不同的頂點集(i in A,j in B),則稱圖G為一個二分圖。由定義可知,二分圖沒有自迴路;零圖,平凡圖可以看成是特殊的二分圖。

2.定義:在二分圖G=<V1,E,V2>中,若V1中的每個結點與V2中的每個結點都有且僅有一條邊相關聯,則稱二分圖G為完全二分圖,記為Ki,j,其中i=|V1|,j=|V2|.

           

3.判斷是否為二分圖的方法:定義法定理:無向圖G=<V,E>為二分圖的充要條件是G的所有迴路的長度均為偶數。

下面進入今天的正題的一些基本概念

1 最大匹配
 在G的一個子圖M中,M的邊集中的任意兩條邊都不依附於同一個頂點,則稱M是一個匹配。選擇這樣的邊數最大的子集稱為圖的最大匹配問題,最大匹配的邊數稱為最大匹配數.如果一個匹配中,圖中的每個頂點都和圖中某條邊相關聯,則稱此匹配為完全匹配,也稱作完備匹配。如果在左右兩邊加上源匯點後,圖G等價於一個網路流,二分圖最大匹配問題可以轉為最大流的問題。解決此問的匈牙利演算法的本質就是尋找最大流的增廣路徑。上圖中的最大匹配如下圖紅邊所示:

                                                   二分圖

2 最優匹配

最優匹配又稱為帶權最大匹配,是指在帶有權值邊的二分圖中,求一個匹配使得匹配邊上的權值和最大。一般X和Y集合頂點個數相同,最優匹配也是一個完備匹配,即每個頂點都被匹配。如果個數不相等,可以通過補點加0邊實現轉化。一般使用

KM演算法解決該問題。

3 最小覆蓋

二分圖的最小覆蓋分為最小頂點覆蓋和最小路徑覆蓋:

①最小頂點覆蓋是指最少的頂點數使得二分圖G中的每條邊都至少與其中一個點相關聯,二分圖的最小頂點覆蓋數=二分圖的最大匹配數;

②最小路徑覆蓋也稱為最小邊覆蓋,是指用盡量少的不相交簡單路徑覆蓋二分圖中的所有頂點。二分圖的最小路徑覆蓋數=|V|-二分圖的最大匹配數;

4 最大獨立集

    最大獨立集是指尋找一個點集,使得其中任意兩點在圖中無對應邊。對於一般圖來說,最大獨立集是一個NP完全問題,對於二分圖來說最大獨立集=|V|-二分圖的最大匹配數。如下圖中黑色點即為一個最大獨立集:

                                                    二分圖


下面來進行匈牙利演算法

初始時最大匹配為空
while 找得到增廣路徑
    do 把增廣路徑加入到最大匹配中去

可見和最大流演算法是一樣的。但是這裡的增廣路徑就有它一定的特殊性,下面我來分析一下。
(注:匈牙利演算法雖然根本上是最大流演算法,但是它不需要建網路模型,所以圖中不再需要源點和匯點,僅僅是一個二分圖。每條邊也不需要有方向。)


圖1是我給出的二分圖中的一個匹配:[1,5]和[2,6]。圖2就是在這個匹配的基礎上找到的一條增廣路徑:3->6->2->5->1->4。我們藉由它來描述一下二分圖中的增廣路徑的性質:

(1)有奇數條邊。
(2)起點在二分圖的左半邊,終點在右半邊。
(3)路徑上的點一定是一個在左半邊,一個在右半邊,交替出現。(其實二分圖的性質就決定了這一點,因為二分圖同一邊的點之間沒有邊相連,不要忘記哦。)
(4)整條路徑上沒有重複的點。
(5)起點和終點都是目前還沒有配對的點,而其它所有點都是已經配好對的。(如圖1、圖2所示,[1,5]和[2,6]在圖1中是兩對已經配好對的點;而起點3和終點4目前還沒有與其它點配對。)
(6)路徑上的所有第奇數條邊都不在原匹配中,所有第偶數條邊都出現在原匹配中。(如圖1、圖2所示,原有的匹配是[1,5]和[2,6],這兩條配匹的邊在圖2給出的增廣路徑中分邊是第2和第4條邊。而增廣路徑的第1、3、5條邊都沒有出現在圖1給出的匹配中。)
(7)最後,也是最重要的一條,把增廣路徑上的所有第奇數條邊加入到原匹配中去,並把增廣路徑中的所有第偶數條邊從原匹配中刪除(這個操作稱為增廣路徑的取反),則新的匹配數就比原匹配數增加了1個。(如圖2所示,新的匹配就是所有藍色的邊,而所有紅色的邊則從原匹配中刪除。則新的匹配數為3。)

不難想通,在最初始時,還沒有任何匹配時,圖1中的兩條灰色的邊本身也是增廣路徑。因此在這張二分圖中尋找最大配匹的過程可能如下:

(1)找到增廣路徑1->5,把它取反,則匹配數增加到1。
(2)找到增廣路徑2->6,把它取反,則匹配數增加到2。
(3)找到增廣路徑3->6->2->5->1->4,把它取反,則匹配數增加到3。
(4)再也找不到增廣路徑,結束。

當然,這只是一種可能的流程。也可能有別的找增廣路徑的順序,或者找到不同的增廣路徑,最終的匹配方案也可能不一樣。但是最大匹配數一定都是相同的。

對於增廣路徑還可以用一個遞迴的方法來描述。這個描述不一定最準確,但是它揭示了尋找增廣路徑的一般方法:
“從點A出發的增廣路徑”一定首先連向一個在原匹配中沒有與點A配對的點B。如果點B在原匹配中沒有與任何點配對,則它就是這條增廣路徑的終點;反之,如果點B已與點C配對,那麼這條增廣路徑就是從A到B,再從B到C,再加上“從點C出發的增廣路徑”。並且,這條從C出發的增廣路徑中不能與前半部分的增廣路徑有重複的點。
比如圖2中,我們要尋找一條從3出發的增廣路徑,要做以下3步:
(1)首先從3出發,它能連到的點只有6,而6在圖1中已經與2配對,所以目前的增廣路徑就是3->6->2再加上從2出發的增廣路徑。
(2)從2出發,它能連到的不與前半部分路徑重複的點只有5,而且5確實在原匹配中沒有與2配對。所以從2連到5。但5在圖1中已經與1配對,所以目前的增廣路徑為3->6->2->5->1再加上從1出發的增廣路徑。
(3)從1出發,能連到的不與自已配對並且不與前半部分路徑重複的點只有4。因為4在圖1中沒有與任何點配對,所以它就是終點。所以最終的增廣路徑是3->6->2->5->1->4。

但是嚴格地說,以上過程中從2出發的增廣路徑(2->5->1->4)和從1出發的增廣路徑(1->4)並不是真正的增廣路徑。因為它們不符合前面講過的增廣路徑的第5條性質,它們的起點都是已經配過對的點。我們在這裡稱它們為“增廣路徑”只是為了方便說明整個搜尋的過程。而這兩條路徑本身只能算是兩個不為外界所知的子過程的返回結果。
顯然,從上面的例子可以看出,搜尋增廣路徑的方法就是DFS,可以寫成一個遞迴函式。當然,用BFS也完全可以實現。

至此,理論基礎部份講完了。但是要完成匈牙利演算法,還需要一個重要的定理:

如果從一個點A出發,沒有找到增廣路徑,那麼無論再從別的點出發找到多少增廣路徑來改變現在的匹配,從A出發都永遠找不到增廣路徑。

要用文字來證明這個定理很繁,話很難說,要麼我還得多畫一張圖,我在此就省了。其實你自己畫幾個圖,試圖舉兩個反例,這個定理不難想通的。(給個提示。如果你試圖舉個反例來說明在找到了別的增廣路徑並改變了現有的匹配後,從A出發就能找到增廣路徑。那麼,在這種情況下,肯定在找到別的增廣路徑之前,就能從A出發找到增廣路徑。這就與假設矛盾了。)
有了這個定理,匈牙利演算法就成形了。如下:

初始時最大匹配為空
for 二分圖左半邊的每個點i
    do 從點i出發尋找增廣路徑。如果找到,則把它取反(即增加了總了匹配數)。
如果二分圖的左半邊一共有n個點,那麼最多找n條增廣路徑。如果圖中共有m條邊,那麼每找一條增廣路徑(DFS或BFS)時最多把所有邊遍歷一遍,所花時間也就是m。所以總的時間大概就是O(n * m)。

程式碼實現

還是用向來擅長的STL實現了
#include <cstdio>
#include <string.h>
#include <vector>
using namespace std;
int const MAX = 10000;
vector<int> G[MAX];
int vis[MAX];
int link[MAX];//代表當前狀態Y中與X相連的點(即d[y] = x)
int n, m;//n為X集合點數,m為Y集合點數(這裡預設點的序號為X到Y依次遞增)

int can(int t)//這一個t保證它是X中的點
{
    for(int i = 0; i < G[t].size(); i++){//從Y中的每一點開始找
        int tmp = G[t][i];
        if(!vis[tmp]){//如果這點沒有被前面幾次找過
            vis[tmp] = 1;
            if(link[tmp] == -1 || can(link[tmp])){//這裡尋找增廣路徑的結束條件就是找到Y中未被配對過的點,如果這點被配對過,那麼就從這點配對的X中的點進行新一輪的can()
               link[tmp] = t;
               return 1;
            }
        }
    }
    return 0;
}

int maxmatch()
{
    int num = 0;
    for(int i = 1; i <= n; i++){
        memset(vis, 0, sizeof(vis));
        if(can(i)){
            num++;
        }
    }
    return num;
}

int main()
{
    while(scanf("%d %d", &n, &m) != EOF){
        for(int i = 1; i <= n; i++)
            G[i].clear();
        memset(link, -1, sizeof(link));
        int s;
        scanf("%d", &s);
        while(s--){
            int a, b;//表示X中的a與Y中的b相連
            scanf("%d %d", &a, &b);//假設輸入的點X和Y分開輸入(都是從1開始)
            G[a].push_back(b+n);
            G[b+n].push_back(a);//由於是無向圖
        }
        printf("%d\n", maxmatch());
    }
    return 0;
}


接下來是一些比較水,一看就知道用最大匹配或者是它的一些小變種來解決的題目把

第一題。。是直接找最大匹配數目的題目。。。找到最大匹配數目,然後與n相比較,如果一樣,輸出YES,otherwise,輸出NO

COURSES

Time Limit: 1000MS Memory Limit: 10000K
Total Submissions: 20759 Accepted: 8191

Description

Consider a group of N students and P courses. Each student visits zero, one or more than one courses. Your task is to determine whether it is possible to form a committee of exactly P students that satisfies simultaneously the conditions:
  • every student in the committee represents a different course (a student can represent a course if he/she visits that course)
  • each course has a representative in the committee

Input

Your program should read sets of data from the std input. The first line of the input contains the number of the data sets. Each data set is presented in the following format:

P N
Count1 Student1 1 Student1 2 ... Student1 Count1
Count2 Student2 1 Student2 2 ... Student2 Count2
...
CountP StudentP 1 StudentP 2 ... StudentP CountP

The first line in each data set contains two positive integers separated by one blank: P (1 <= P <= 100) - the number of courses and N (1 <= N <= 300) - the number of students. The next P lines describe in sequence of the courses �from course 1 to course P, each line describing a course. The description of course i is a line that starts with an integer Count i (0 <= Count i <= N) representing the number of students visiting course i. Next, after a blank, you抣l find the Count i students, visiting the course, each two consecutive separated by one blank. Students are numbered with the positive integers from 1 to N.
There are no blank lines between consecutive sets of data. Input data are correct.

Output

The result of the program is on the standard output. For each input data set the program prints on a single line "YES" if it is possible to form a committee and "NO" otherwise. There should not be any leading blanks at the start of the line.

Sample Input

2
3 3
3 1 2 3
2 1 2
1 1
3 3
2 1 3
2 1 3
1 1

Sample Output

YES
NO

程式碼

#include<cstdio>
#include <string.h>
using namespace std;
int mp[500][500];
int n, m;//n表示課程數,m表示學生數
int link[400];//記錄當前與y節點相連的x的節點
int vis[400];
int can(int t)
{
    int i;
    for(i = n + 1; i <= n + m; i++){
        if(!vis[i] && mp[t][i]){
            vis[i] = 1;
            if(link[i] == -1 || can(link[i])){
                link[i] = t;
                return 1;
            }
        }
    }
    return 0;
}
int maxmartch()
{
    int num = 0;
    memset(link, 0xff, sizeof(link));
    for(int i = 1; i <= n; i++){
        memset(vis, 0, sizeof(vis));
        if(can(i))
            num++;
    }
    return num;
}
int main()
{
    int cases;
    scanf("%d", &cases);
    while(cases--){
        memset(mp, 0, sizeof(mp));
        scanf("%d %d", &n, &m);
        for(int i = 1; i <= n; i++){
            int s;
            scanf("%d", &s);
            while(s--){
                int k;
                scanf("%d", &k);
                mp[i][k+n] = mp[k+n][i] = 1;
            }
        }
        if(maxmartch() == n)
            printf("YES\n");
        else
            printf("NO\n");
    }
    return 0;
}

未完待續。。。