【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万,而且须要最大字典序输出,只须要把以前的邻接矩阵改为邻接表便可提升速度,

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

 


 

窃觉得理解透彻了,将思路所有放上来,可能有些啰嗦。

写到后面脑子很乱,不知道该如何表达,不对地方还请指正 

相关文章
相关标签/搜索