【OI】二分圖最大匹配

所謂二分圖,是能夠分爲兩個點集的圖;算法

所謂二分圖最大匹配,是兩個點集之間,每兩個不一樣點集的點鏈接,每一個點只能連一個點,最大的鏈接數就是最大匹配。數組

 

如何解最大匹配,須要用到匈牙利算法。ide

 

另:本文寫了不少細節,有的地方比較囉嗦,請大佬放過函數


 

匈牙利算法是一個遞歸的過程,它的特色,我以爲能夠歸爲一個字:「讓」。spa

 

 

 

例如這張圖,按照匈牙利算法的思路就是:翻譯

1.1與5匹配,5沒有被標記,將5標記,記錄1與5匹配debug

2.清空標記code

3.2與5匹配,5沒有被標記,將5標記,發現5已經與1匹配,在[此處]從新遞歸1:blog

  ①1與5匹配,發現5已經被標記,跳出遞歸

  ②1與7匹配,發現7沒有被標記,將7標記,記錄1與7匹配,返回成功

4.回到2與5匹配的[此處],發現返回成功,則直接記錄2與5匹配

5.清空標記

6.3與5匹配,5沒有被標記,將5標記,發現5已經與2匹配,在[此處]從新遞歸2:

  ①2與5匹配,發現5已經被標記,跳出

  ②2沒有其餘鏈接的邊了,返回失敗

7.回到3與5匹配的[此處],發現返回失敗,繼續查找與3鏈接的邊

8.3與6匹配,6沒有被標記,將6標記,記錄3與6匹配

9.清空標記

9.4與7匹配,7沒有被標記,將7標記,發現7已經與1匹配,在[此處]從新遞歸1:

  ①1與5匹配,5沒有被標記,將5標記,發現5已經與2匹配,在[此處A]從新遞歸2:

    ①2與5匹配,發現5已經被標記,跳出

    ②2沒有鏈接的邊了,返回失敗

  ②回到1與5匹配的[此處A],發現返回失敗,繼續查找1鏈接的邊

  ③1與7匹配,發現7已經被標記,跳出

  ④1沒有能夠鏈接的邊了,返回失敗

10.回到4與7匹配的[此處],發現返回失敗,繼續查找與4鏈接的邊

11.4與8匹配,8沒有被標記,將8標記,記錄4與8匹配

12.清空標記

13.左邊的點集枚舉完畢,從記錄中獲得:1與7匹配,2與5匹配,3與6匹配,4與8匹配

 

這就是匈牙利算法(這就是人腦編譯器嗎)

 

用人話來講,就是

1:誒,你看我找到我鏈接的第一個,是一個沒人佔據的點啊,我和5匹配吧

2:誒,你看我找到我鏈接的第一個就是5,居然被1佔據了!可惡,1你再去找找有沒有別的邊去匹配!

1:我要匹配5!

2:這是我要匹配的!

1:好吧,我看看,我鏈接的第二個,是一個沒人佔據的邊啊,我和7匹配吧

2:好棒啊,那我就和5匹配了

3:我鏈接的第一個邊是5,竟然被2佔據了,2你去看看有沒有別的邊匹配啊

2:好,我第一個鏈接的點就是5,我要鏈接5!

3:我要和5匹配!泥奏凱!

2:好吧,那我鏈接的第二個點。。沒有第二個點,我只有匹配5了!!!

3:我去,這麼不湊巧,那好吧,我只好找找我鏈接的第二個點了,只有6了,6尚未被人佔據,我捷足先登,嘿嘿嘿

4:我第一個鏈接的點是7,居然被1佔據了, 可惡,1給我等着,你去看看有沒有別的邊

1:我第一個鏈接的點是5,可是被2佔據了,若是想讓我給你挪騰地方的話,我只好先讓2換個地方

2:那麼我第一個鏈接的點是5,1你要用的話我就不能夠匹配它。我沒有第二個鏈接的點,所以1對不起,我不能給你挪騰地方

1:那好吧,那麼我第二個鏈接的點是7——

4:我要這個點啊!原本個人目的就是讓你挪騰地方離開7啊

1:那我沒地能夠挪騰了,愛能莫助啊~~~

4:那好吧,看看我鏈接的第二個點8,看來這個點沒有被人佔據,那麼我就和它匹配

至此,全部的點都找到歸屬了。

 

(這tm不就是翻譯過來嗎,哪有正常人這麼說話)

咳咳咳,anyway,匈牙利算法就是這樣一個神奇的算法。

 

 總結一下,從某種意義上來講,匈牙利算法算是一個動態規劃。

爲了讀者理解方便,這裏規定:咱們枚舉的點集用小寫字母表示,另外一個點集用大寫字母表示。

由於由它的遞歸結構決定,只要一個點當前要匹配的點(設它爲A)與另外的點(設它爲B)要與同一個點(設它爲c)匹配(爲何它們都要與c匹配的緣由就是A是按照順序依次匹配的,每個A鏈接的點都要被依次嘗試,因爲匈牙利算法的內容決定的它的性質,所以不管順序如何最後獲得的都是最優的局面),那麼A能夠在B找到除了c之外的其它匹配的前提下達成對於A的最優局面,即A匹配c,B匹配另外的點。這樣原來的匹配數不變,又增長了一條匹配。

若是B經過遞歸沒法找到其它匹配,那麼若是捨棄B這個匹配換上A的匹配,並不會增長匹配數。所以,這個策略是最優的。

可是這樣說還不夠,爲何就能保證A之前的匹配都是最優的呢?這樣就必須說說B的遞歸匹配過程。

A要匹配c,那麼讓B與除了C之外的點匹配。若是B直接找到了未匹配的點(除了c,下同),那麼直接匹配。若是B沒有直接找到未匹配的點,那麼B鏈接的邊必定都是已經匹配其它點的。那麼B就會嘗試改變B要匹配的點(設它爲d)的匹配的點(設它爲E)的匹配,與A讓B更換匹配同樣,讓E更換匹配爲除了d之外的匹配點,這樣B就能夠獲得d這個點的匹配了。而後,E重複B的過程......如此這般,若是一直找不到能夠直接匹配的點的話,能夠回溯到第一次匹配。這樣,全部的匹配都會更換爲:「在不改變原有匹配數的狀況下,對A最優的局面,也就是對A匹配c最優的局面」,所以,每次匹配,老是會形成對當前局面的最優的匹配,若是局部不是最優,那麼一旦涉及到須要這塊局部最優的時候,這塊將會一樣被回溯到而後更改成最優。(這裏的最優都是指的對當前局面的最優)。

固然,相信有聰明的同窗已經想到,若是這樣匹配的話,萬一整個二分圖不是聯通圖怎麼辦。很簡單,若是按照上面代碼的寫法,每一個連通塊至關於一個二分圖,每一個二分圖的匹配按照上面的寫法老是最優的,最後的統計最大匹配只須要把每一個連通塊的最大匹配相加就能夠了。

 

太長不看版:牽一髮,動全身。每一次的嘗試匹配的操做都會形成對當前整個圖的匹配的調整,不管以前是怎樣的圖,最後都會被調整到對當前匹配最有利的圖。

 

 

至於如何證實它的正確性,必需要這樣一個東西來幫助咱們:

增廣路,它的性質是:(匹配點/邊用1表示,非匹配點/邊用0表示,N表示點/邊的個數)

第一條邊是非匹配邊,而後到匹配邊,而後到非匹配邊......最後的邊必定是非匹配邊,而且邊的個數必定是奇數。(01010101...0,N mod 2 ≠ 0)

那麼匈牙利算法的實質,或者說另外一種形式,就是不斷尋找增廣路來擴大匹配。

(我看的書上並無增廣路和匈牙利算法的關係,那麼在這裏詳細說明是如何尋找增廣路的)

在上面的描述中,咱們知道,匈牙利算法的基本結構是枚舉一個點集,經過上述方式「讓」出最大匹配。

可是在「讓」的過程當中,咱們發現,以前的操做,實際上都符合尋找增廣路的方法。

例如,咱們在匹配2的過程當中(請回顧以前的模擬匈牙利算法的那段),

增廣路的第一個點是2,接着通過那些操做,與2匹配的點是5,那麼第二個點就是5。而以前與5匹配的點是1,1如今又7匹配。

則爲:2->5->1->7

若是咱們把更換匹配以前的匹配邊稱做匹配邊,會發現:

2->5在更換匹配以前沒有匹配,爲非匹配邊。

5->1在更換匹配以前是匹配的,爲匹配邊。

1->7在更換以前是沒有匹配的,爲非匹配邊。

正好符合咱們的增廣路定義!其中,1->7就是咱們增長的邊。

爲何會這樣?

讓咱們再來解說一次,用紅色和藍色來區分增廣路和「讓」的方法:

爲了說明方便,這裏假設最後匹配到了能夠直接匹配的點,也就是說增廣路發現成功

 

首先,增廣路的第一個邊一定是非匹配邊。

咱們枚舉點集的時候一定沒有枚舉過當前枚舉的點(設它爲P),那麼P以前沒有與任何邊匹配,因此與P相連的邊是非匹配邊,設與P相連的點爲i。

若是i原來不是匹配點,那麼這條增廣路已經結束,不存在第二條邊,最後一條邊是非匹配邊。

而後,增廣路的第二條邊一定是匹配邊,最後一條邊一定是非匹配邊。

同上,若是P鏈接的i原來不是匹配點,則增廣路結束,第二條邊不存在,而第一條邊也是最後一條邊,也符合定義。

若是i原來是匹配點,設X爲i原來匹配的點,由於P爲非匹配點,則X≠P,則X一定是這條增廣路的第三個點,則這條邊,也就是第二條邊,是匹配邊。

接着,增廣路的第三條邊一定是非匹配邊

這兒分兩種狀況,第一是X更換到的點(設它爲y)是非匹配點,能夠直接匹配,那麼由於y是非匹配點,則X->y是非匹配邊,符合定義。

第二是y已經匹配了,因爲X原來是匹配點,而一個點只能匹配一個點,X已經與i匹配,則y原來一定與X不匹配,則這條邊(X->y)原來一定不是匹配邊。符合定義。

...剩下同理

 

所以,只要最後找到了未匹配點,都算找到了增廣路。

---------------------------

模板題HDU - 1083

#include <cstdio>
#include <cstring>

const int MaxN = 500;

int ask[MaxN];
int vis[MaxN][MaxN];
int matched[MaxN];
int n,m;//n:課程人數,m:學生人數 
int ans;

bool find(int from)
{
    for(int i = 1; i <= m; i++){
        if(vis[from][i]){
            if(!ask[i]){
                ask[i] = 1;
                
                if(!matched[i] || find (matched[i])){
                    matched[i] = from;
                    return 1;
                }
                
            }
            
        }
        
    }
    return 0;
    
}

void match(){
    int count = 0;
    
    memset(matched,0,sizeof(matched));
    
    for(int i = 1; i <= n; i++){
        memset(ask,0,sizeof(ask));
        
        if(find(i))
            count ++;
        
    }
    
    ans = count ;
}


int main()
{
    int data_p;
    scanf("%d",&data_p);
    while(data_p--){
    
    scanf("%d%d",&n,&m);
    
    for(int i = 1; i <= m; i++){
        int num = 0;
        
        scanf("%d",&num);
        for(int j = 1; j <= num; j++){
            int tmp;
            scanf("%d",&tmp);
            vis[i][tmp] = 1;
            
        }
    }
    
    match();
    
    if(ans == n){
        printf("YES\n");
    }
    else{
        printf("NO\n");
    }
    memset(vis,0,sizeof(vis));
    ans = 0;
    }
    
    return 0;
}
View Code

先在match函數中枚舉每一個左集的點,每一個左集的點調用Find函數。

Find中,枚舉右集的點,找匹配,將匹配到的點標記,若是這個標記了的點沒有被匹配或者遞歸上去能找到其餘點匹配,那麼就把當前點匹配。

最後,記錄matched數組中的個數,即爲最大匹配。

---------------------------

 HDU - 3729

#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>

const int MaxN = 100010;

struct EDGE{
    int to,nxt;
}edge[MaxN];
int head[100];//[]點最後一個鏈接的邊 
int e_num;//邊的數量 

void add(int u,int v){
    edge[++e_num].to = v;
    edge[e_num].nxt = head[u];
    
    head[u] = e_num;
}

int n,ans;
bool ask[MaxN];
int matched[MaxN];
int q[MaxN];

bool Find(int u){
    for(int i = head[u]; i ; i = edge[i].nxt){
        //if(edge[u][i]){
            if(!ask[edge[i].to]){
                ask[edge[i].to] = 1;
                
                //printf("new : %d->%d\n",u,edge[i].to);
                
                if(!matched[edge[i].to] || Find(matched[edge[i].to])){
                    matched[edge[i].to] = u;
                    //printf("best match! %d|%d\n",u,i);
                    
                    //printf("matched[%d] = %d\n",i,matched[i]);
                    
                    return true;
                }
            }
        //}
    }
    
    return false;
}

void match(){
    memset(matched,0,sizeof(matched));
    
    int count = 0;
    
    for(int i = n; i >= 1; i --){
        memset(ask,0,sizeof(ask));
        if(Find(i))
            count ++;
    }
    
    ans = count;
}

int main()
{
    
    int data_n;
    scanf("%d",&data_n);
    while(data_n--){
        
        memset(edge,0,sizeof(edge));
        memset(head,0,sizeof(head));
        e_num = 0;
    
    scanf("%d",&n);
    for(int i = 1; i <= n; i++){
        int x1,x2;
        scanf("%d%d",&x1,&x2);
        for(int j = x1; j <= x2; j++){
            //edge[i][j] = 1;
            add(i,j);
        }
    }
    /*debug
    for(int  i = 1; i <= n; i++){
        for(int j = head[i] ; j ; j = edge[j].nxt){
            printf("%d - > %d\n",i,edge[j].to);
            
        }
        
    }
    //debug*/
        
    match();
        
    printf("%d\n",ans);
        
    int cnt = 0;
    
    memset(q,0,sizeof(q));
        
    for(int j = 1; j <= 100000; j++){
        if(matched[j]){
            q[++cnt] = matched[j];
        }
    }
    
    std::sort(q+1,q+cnt+1);
    
    for(int j = 1; j <= cnt; j++){
        printf("%d",q[j]);
        if(j != cnt)
            printf(" ");
            
        //printf("|end|");
    }
    
    
    //if(data_n != 0)
        printf("\n");

}

    return 0;
}
/*
2
4
5004 5005
5005 5006
5004 5006
5004 5006
7
4 5
2 3
1 2
2 2
4 4
2 3
3 4
*/
View Code

幾乎是模板題,只不過數據有10萬,而且須要最大字典序輸出,只須要把以前的鄰接矩陣改爲鄰接表便可提升速度,

只要把左集倒序枚舉便可獲得最大字典序答案。

 


 

竊覺得理解透徹了,將思路所有放上來,可能有些囉嗦。

寫到後面腦子很亂,不知道該如何表達,不對地方還請指正 

相關文章
相關標籤/搜索