二分图的匹配——匈牙利算法

什么是匹配

匹配:在图论中,一个「匹配」是一个边的集合,其中任意两条边都没有公共顶点。
最大匹配:一个图所有匹配中,所含匹配边数最多的匹配,称为这个图的最大匹配。

二分图的匹配:给定一个二分图G,在G的一个子图M中,M的边集{E}中的任意两条边都不依附于同一个顶点,则称M是一个匹配。
二分图的最大匹配:所有匹配中包含边数最多的一组匹配被称为二分图的最大匹配,其边数即为最大匹配数。

匈牙利算法

前面的染色法是判断一个图是否是二分图;
匈牙利算法就是在是二分图的基础上,求二分图的最大匹配的算法。

二分图将顶点分成两个集合A和B,集合A和B内部之间没有边,A中的点到B中的点存在边。(A中的点x到B中的某些点之间存在边,可能一条,可能多条,也可能没有)
最大匹配就是尽可能的可以多选择边,使得A中的点和B中的点匹配,且只能一对一匹配,即匹配过的不能再匹配,使任意两条边都没有公共顶点。

二分图一般都是无向图,双向匹配,我们在试图匹配的时候,只从一个方向到另一个方向匹配就行:以下做题及代码都是假设从集合A出发到集合B中区匹配的。因此,虽然是无向图,我们可以只存储从A->B的边,而不存储B->A的边,这样可以自动的将顶点分成两个集合,而且两个集合内结点的编号都可以从1开始

匹配过程

如果你想找的妹子已经有了男朋友,
你就去问问她男朋友,
你有没有备胎,
把这个让给我好吗?
他能让就让,
要是让不了,他们就是真爱。
你就找你的下一个备胎。

假设此时已暂时有匹配关系a1----b1,a2----b2,A中的顶点a1有指向b1、b2的点,a2只有指向b2的边,a3有指向b1、b3、等……的边,我们以A中的顶点a3的匹配为例:

假设已暂时有匹配关系a1----b1,a2----b2,那么a2就不能有指向b1的边,否则就不符合匈牙利算法核心1。

下面分析过程:

给a3找匹配对象,a3先去找b1,发现b1已经被匹配了,但是此时a3不是去找b3,而是让b1去找他的匹配对象a1,看看a1能不能找别的点匹配,此时回到了给a1找匹配对象的问题,但是匹配对象b1被禁了,所以a1不能再去找b1,接下来去找b2,b2没被禁,可以找,但是b2已经有了匹配对象a2,然后再去问a2能不能换个匹配对象,此时又回到了给a2找匹配对象的问题上与此同时点b1、b2都被禁了因为b1点是a3点想要的,b2点是a1点想要的,都被待定了但是a2已经没有再往后退的选择了此时a2和b2是真爱,永久匹配,被锁定。然后再往回告诉a1,b2不行,你再找下一位吧,但是a1也是没有了回退的余地,所以a1和b1也是真爱,永久匹配,被锁定。然后告诉a3,b1不行,你再找下一位吧,然后a3开始找b3……

上面的匹配可以用match数组,被禁可以用st数组表示状态,是否锁定可以用vis数组表示
match[j]=i 表示集合A中的 点i 和集合B中的 点j 暂时匹配
st[j]=true表示顶点 j 暂时被禁
vis[j]=true表示match[i]=j中的 i 和 j 是真爱,后面再来的顶点不要再想着占 顶点j 了。

假设a1只有指向b1、b2的边,a2只有指向b1、b2的边,a3有指向b1、b2、b3的边
先给a1选择,match[a1]=b1;
然后a2选择,发现b1被占用,然后强迫a1让位,此时有match[a1]=b2; match[a2]=b1;
然后a3选择,b1被占用,然后a3强迫a2让位,a2不能选择b1,只能选择b2,然后a2强迫a1让位,此时a1不能选择b1、b2,a1没有回退的余地,a1和b2被锁定,把消息告诉a2后,a2也没有回退的余地,a2和b1被锁定。
当a3找上b2时,直接告诉a3,b2被锁定了,没戏,你找下一位吧。
接下来a3找到b3,有match[a3]=b3;

再举这样一个例子是想说明匈牙利算法的核心:

  1. 先匹配的都是暂时的,后匹配的才是最有可能获得的,因为先匹配的都要尽可能的让后匹配的,除非先匹配的到了退无可退的地步。
  2. 这是一个递归的过程,递归结束的那次匹配并不仅仅是因为这次匹配的顶点出度为1,只有一条边伸向另一个集合,这只是其中一种情况,一般情况则是这个顶点已经到了退无可退的地步,不能再给别的顶点让了,再让自己就没有了。

题目描述

给定一个二分图,其中左半部集合包含n1个点(编号1-n1),右半部集合包含n2个点(编号1-n2),二分图共包含m条边。

数据保证任意一条边的两个端点都不可能在同一部分中。

请你求出二分图的最大匹配数。

输入格式
第一行包含三个整数 n1、 n2 和 m。

接下来m行,每行包含两个整数u和v,表示左半部点集中的点u和右半部点集中的点v之间存在一条边。

输出格式
输出一个整数,表示二分图的最大匹配数。

数据范围
1≤n1,n2≤500,
1≤u≤n1,
1≤v≤n2,
1≤m≤105

输入样例:

2 2 4
1 1
1 2
2 1
2 2

输出样例:

2

算法实现

#include <iostream>
#include <cstring>

using namespace std;

#define read(x) scanf("%d",&x)
const int N=510,M=1e5+10;  //虽是无向图,但是存储单方向的边A->B即可,到时候从A找点匹配B
int h[N],e[M],ne[M],idx;
int match[N]; 
bool vis[N],st[N];
//三个数组维护的含义见上面或下面
int n1,n2,m;
"n1+n2是总顶点数,n1是A集合中顶点数,n2是B集合中顶点数,由题目要求限制"

void add(int a,int b)
{
    
    
    e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}

int find(int v)
{
    
    
    for (int i=h[v];~i;i=ne[i]) {
    
    
        int j=e[i];
        if (!vis[j] && !st[j]) {
    
    
            if (match[j]==0) {
    
    match[j]=v; return true;}
            else {
    
    
                st[j]=true;
                if (find(match[j])) {
    
    match[j]=v; return true;}
                else vis[j]=true;
            }//此时说明B堆中的j结点与A堆中的结点match[j]是板上钉钉的匹配了,不能再改了,
        }    //所以后面的点再匹配就不用再找我了,
    }
    return false;
}

int main()
{
    
    
    memset(h,-1,sizeof h);
    read(n1),read(n2),read(m);//A堆顶点数n1,B堆顶点数n2,边数m
    int a,b;
    while (m--) {
    
    
        read(a),read(b);
        add(a,b);"两个堆中结点编号重复,这里无影响,也可以add(a,n1+b);"
    }
    int res=0; //统计最大匹配的边数
    for (int i=1;i<=n1;i++) {
    
    
        memset(st,false,sizeof st); //每次初始化一下st数组,重新禁止一些匹配。
        if(find(i)) res++; 
    }
    printf("%d",res);
    
    return 0;
}

match数组:
遍历A堆,存储A->B的边。此时match[i]=j;表示B堆中顶点i与A堆中的顶点j匹配了,由于图中顶点的下标都是从1开始的,所以当match[i]=0时,表示还没有匹配到。

st数组:
st数组的作用是当一个左边的点匹配到右边某个已经有对象的点时,右边的点对应的那条边的左边的点,去寻在新的“对象“时,不再去找原来的右边的那个点,由于多次递归,每次递归前禁止的点都要记录上,所以创建一个st数组,每次开始新的匹配时,又要重置st数组为false,重新禁止

vis数组:
vis数组用于记录那些B堆中没有选择的点,当B堆中的点只有最后一种情况和A堆中的点匹配时,它就被锁定了,所以它也就没有find(match[ ])的必要了。

初级版本,想用 if (!vis[j] && match[j]!=x) 中的 match[j]!=x 代替st数组的功能,但由于每次递归都会禁止一个j,而这样写只会禁止当前的j,对之前的j没有作用了,容易造成死循环。爆栈。

 bool find(int x)
{
    
    
    for (int i = h[x]; i != -1; i = ne[i])  {
    
    
        int j=e[i];
        if (!vis[j] && match[j]!=x) {
    
    
            if (match[j]==0) {
    
    match[j]=x; return true;}
            else {
    
    
                if (find(match[j])) {
    
    match[j]=x; return true;}
                else vis[j] = true;
            }                     
        }
    }
    return false;
}

第5行判断处不一样,这是错误的。

猜你喜欢

转载自blog.csdn.net/HangHug_L/article/details/114106714
今日推荐