Syins的二分图匹配理解

二分图是什么?

定义

二分图又称作二部图,是图论中的一种特殊模型。 设G=(V,E)是一个无向图,如果顶点V可分割为两个互不相交的子集(A,B),并且图中的每条边(i,j)所关联的两个顶点i和j分别属于这两个不同的顶点集(i in A,j in B),则称图G为一个二分图。

理解

在这里插入图片描述
其实就是把点分为u,v两块,而其内部不存在连线。

二分图的判定

我们可以这样想,从任意一个点出发,沿着给定的边走
如果该图是一个二分图,那我们必定是在走交错路的,也就是一下在u这边,一下在v这边
不会从u中的点开始,下一个点仍然是u里的点。
那要怎么做就很清楚了
从一个点开始,给它上一个状态,比如0,到下一个点记一个相反状态比如1
为什么要相反状态呢?
这是因为方便改变值
比如这里提到的0和1就可以通过"!"(非 运算符)来更新,也可以用1-x来更新
如果下一个点的状态跟当前点重复了,那说明不是二分图
这个很好理解:可以看一下图👇
在这里插入图片描述

代码

在我看来代码挺重要,但是也不是那么重要,我还是更看重理解一些,所以给了个3级标题233

#include<bits/stdc++.h>

using namespace std;

const int N=505;

bool f=true;//答案
bool G[N][N];//边
int m,n,pd[N+5];//边数,点数,结点标记
queue<int>q;
void eft_bfs(int x)//用bfs判断
{
    q.push(x);//初始点
    pd[x]=0;//标记为0
    while(!q.empty())
    {
        int p=q.front();//取出首结点
        q.pop();
        for(int i=0;i<n;++i)//枚举
        {
            if(G[p][i])
            {
                if(pd[i]==pd[p])//如果枚举出的结点本来就有标记,而且与该结点一样,则形成环,非二分图
                {
                    f=false;
                    return;
                }
                else if(pd[i]==-1)
                {
                    pd[i]=1-pd[p];//1-0=1....1-1=0
                    q.push(i);
                }
            }
        }
    }
}
int readin()
{
    int x=0,f=1;
    char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
    return x*f;
}
int main()
{
    memset(pd,-1,sizeof pd);//初始化
    n=readin(),m=readin();
    for(int i=0;i<m;++i)
    {
        int u,v;
        u=readin(),v=readin();
        G[u-1][v-1]=G[v-1][u-1]=true;
    }
    for(int i=0;i<n;++i)//枚举,考虑有不连通的情况
    {
        if(pd[i]==-1)eft_bfs(i);
        if(!f)break;
    }
    if(!f)puts("No");
    else puts("Yes");
    return 0;
}
/*
9 8
1 6
2 6
2 7
3 8
3 9
4 9
5 7
5 9
*/

二分图匹配(匈牙利算法)

什么是匹配和我们要做什么

匹配

给定一个二分图G,在G的一个子图M中,M的边集{E}中的任意两条边都不依附于同一个顶点,则称M是一个匹配。

极大匹配

无法再通过增加未完成匹配的边的方式来增加匹配的边数的匹配就是极大匹配

最大匹配(也就是我们现在要得到的)

所有极大匹配当中边数最大的一个匹配。
通俗的说就是匹配数最大的匹配

增广路(关键)

网上有些博客是描述为”腾“,说的其实不错,但实际上还是求增广路,不过是学会之后的一个理解罢了。我个人感觉还是从增广路出发更明白些
上面是废话

什么是增广路呢?
若P是图G中一条连通两个未匹配顶点的路径,并且属于M的边和不属于M的边(即已匹配和待匹配的边)在P上交替出现,则称P为相对于M的一条增广路径(举例来说,有A、B集合,增广路由A中一个点通向B中一个点,再由B中这个点通向A中一个点……交替进行)。

通俗的说就是从一个未匹配的点开始,在u,v两块地方走交错路
而且最后一个点也必须是未匹配点

鼠标绘图,能看就行233
在这里插入图片描述
这里红线是已经连接的,那么增广路可以是1-2-3-6(全黑线就是未匹配)

性质

观察上面的增广路可以发现红线(已匹配边)为1条,黑线(未匹配边)为2条
那么取反之后呢?就变成红2黑1了
诶,那不就是边数增加了吗?
在这里插入图片描述
仔细想想其实不难证明:增广路的未匹配边比匹配边多一条(不然也不叫增广路了

也就是说如果有增广路存在,那么取反后必然会增加边数
这也就是匈牙利算法的主要想法
这里其实就可以说讲完了匈牙利算法
但还是要往下讲些详细的

算法流程

根据刚才的增广路性质
我们要找最大匹配也就是找增广路有多少条
这里如果前面没看进去的话有可能无法理解
这么说吧,一开始所有边都是未匹配的边,也就是一开始是0匹配数,而找到一条增广路意味着我们可以取反来让匹配数+1;
所以说找到一条增广路就可以增加一个匹配数,也就是增广路的数量就是最大匹配数
怎么找呢?
dfs是一个不错的选择
从X部一个未匹配的顶点u开始,找一个未访问的邻接点v(v一定是Y部顶点)。
对于 v,分两种情况:
(1)如果v未匹配,则已经找到一条增广路。
(2)如果v已经匹配,则取出v的匹配顶点w(w一定是X部顶点),边 (w,v)目前是匹配的,根据“取反”的想法,要将(w,v)改为未匹配, (u,v)设为匹配,能实现这一点的条件是看从w为起点能否新找到一 个未匹配点。如果行,则u-v-P’ 就是一条以u为起点的增广路径。
几个注意点
会不会死循环?
不处理当然会,就像下面这个图👇(可以自行模拟)
在这里插入图片描述
所以要开一个标记的数组
另外一个点就是给的图,它不一定是全连通的
这么说吧,它可能是多个连通的二分图放一起的
所以要跑一个for循环来判断所有的点
当然,如果该点在之前的点求增广路的时候就已经有匹配了,那么当然不必判断,所以这里可以有个标记的东西

代码

主函数就建图和答案处理

#include<bits/stdc++.h>
using namespace std;
#define N 120
int jlx[N],jly[N];
int n,m,k,w[N][N],match[N];
bool sy[N];
bool c[N];
bool search_path(int u)
{
    for(int v=0;v<m;++v)
    {
        if(w[u][v]&&(!sy[v]))
        {
            sy[v]=true;
            if(match[v]==-1||search_path(match[v]))
            {
                match[v]=u;
                c[u]=true;
                return true;
            }
        }
    }
    return false;
}
int Hungary()
{
    int jg=0;
    memset(c,false,sizeof c);
    memset(match,-1,sizeof match);
    for(int i=0;i<n;++i)
    {
        if(!c[i])
        {
            memset(sy,false,sizeof sy);
            if(search_path(i))jg++;
        }
    }
    return jg;
}

最优匹配(K_M算法)

很多时候各个边的优先级是不一样的,也就是他们有不同的权重,这个时候就不能简单的应用匈牙利算法了
现在就来看看K_M算法

前置

——————————————————————————————————————————————————
这个算法的关键思想其实是尽量取大边
先取最大的情况,如果不行,那么把减小最少的一种情况拿来考虑

——————————————————————————————————————————————————
我觉得这个是非常重要的

完备匹配

完备匹配就是所有的点都存在匹配,这也就意味着完备匹配一定是最大匹配,同时如果二分图存在完备匹配那么最大匹配也一定是完备匹配

最优匹配

就是带权最大匹配,也就是我们的解决目标

一个定理(关键)

M是一个带权完全二分图G的一个完备匹配,给每个顶点一个可行顶标(第i个x顶点的可行标用lx[i]表示,第j个y顶点的可行标用ly[j]表示),如果对所有的边(i,j) in G,都有lx[i]+ly[j]>=w[i,j]成立(w[i,j]表示边的权),且对所有的边(i,j) in M,都有lx[i]+ly[j]=w[i,j]成立,则M是图G的一个最佳匹配。
我也不知道算不算定理奥(滑稽)
这里初略的证明一下,因为顶标满足在G中x顶标加上y顶标是要大于等于两点间的边权的
而在其完备匹配中,有满足x顶标加上y顶标等于两点间的边权
前面说了完备匹配是所有点都匹配了,那么,这里就满足了两个条件:1.单个边权是M中的大于等于G中的。2.M中形成的匹配(也就是要加起来的边)是最多的。
所以很简单就得到了:最多的最大边权相加那就是最优匹配。
无非就是存不存在M的问题了。

算法流程

先不考虑其他的,就根据这个定理来说,我们很容易想到这样的做法:
1 要设顶标

2 要该顶标下是否存在完备匹配(匈牙利算法实现)

3 如果不存在那么修改顶标

顶标设立

首先想到的就是怎么设一个顶标
前面提到对所有的边(i,j) in G,都有lx[i]+ly[j]>=w[i,j]成立
那我们就让lx[i]=max(w[i,j]);让ly[i]=0;
你当然可以这么想,不过从我前置目录下写的关键思想方面考虑,会发现更加合理的解释
这么说吧:我们要最优匹配,就是要最大边权和
那怎么取最大呢?
因为一个点只能取一条边(一个匹配)所以我们要考虑最大的情况就是每个点都与权最大的边相连(当然这个最大是与该点相连的边)
也是因此我们让lx[i]为与i点相关的最大边权值

代码

啊,没错这里就是在水长度

for(int i=0;i<n;++i)//初始化顶标,lx(i)变为与xi关联的边的边权最大值,ly为0
    {
        ly[i]=0;
        lx[i]=-0x7fffffff;
        for(int j=0;j<n;++j)
        {
            if(lx[i]<w[i][j])lx[i]=w[i][j];
        }
    }

怎么找完备匹配

第一个问题解决了(也许还没有(滑稽))
那么第二个点就是找完备匹配了
怎么找呢?
完备匹配就是所有点都匹配了
那我们完全可以去找,每个点是否可以有匹配
这个很容易想到匈牙利算法
之前提过增广路的性质,取反之后会加1
然后还要想到的一点是:取反之后,未匹配点匹配了。
也就是说我们从未匹配点出发,如果能找到增广路,那么我们取反之后该点就会有匹配,所以只要跑增广路就可以判断每个点是否有匹配。
当然我们要的不只是匹配,而是要满足lx[u]+ly[v]==w[u][v]的完备匹配
所以还要加上这个条件来限制
具体流程这里就不赘述了,我寻思着已经是非常详细了(如果还没理解就看代码将就一下吧/(ㄒoㄒ)/~~)

代码

假设i在y里
那么match[i]就是与i形成匹配的在x里的点

bool search_pp(int u)
{
    sx[u]=true;
    for(int v=0;v<n;++v)
    {
        if(!sy[v]&&lx[u]+ly[v]==w[u][v])//这里sy同样有防止死循环的效果
        {
            sy[v]=true;
            if(match[v]==-1||search_pp(match[v]))//如果该点未匹配或者能找到下一个未匹配点,同匈牙利算法
            {
                match[v]=u;
                return true;
            }
        }
    }
    return false;
}

找不到的情况下怎么修改

当然理想情况是很少的,现实很残酷
那么如果不存在完备匹配我们就要修改顶标(虽然说用K_M算法其实必须又完备匹配,但我们毕竟还有个限制条件)
怎么修改呢?
你可以这么理解:
因为我们一开始设置的是最大的情况
所以现在肯定是要进行减少
那么对于一次寻找的过程有4种情况
1:x在找增广路时找到过,y也是,lx[i]-inc+ly[i]+inc是不变的
2:x在找增广路时找到过,y没有,lx[i]-inc+ly[i]这样是减少的,也是有可能的
3:x没有,y有,lx[i]+ly[i]+inc是增加的,也是不可能的
4:都没有当然也是不变的
很多人可能看不懂(比如我/(ㄒoㄒ)/~~)
这里我说一下:
为什么找增广路时找到过的lx要加而ly要减,我们要改变条件来得到匹配也就意味着顶标的值要改变,那怎么改呢?
lx之前说了取了最大,那不成立怎么办,当然是减小,而lx小了ly要加,不然这两点之间的值就变了,所以有上面4种情况的考虑
而为什么没有找到过的不用改?都没见到过为什么要管他们(滑稽)而且本来他们相加也不是边权,所以不必改
那么inc取什么呢?
前面提到过一个条件lx[i]+ly[j]>=w[i][j]
那么lx[i]-inc+ly[j]>=w[i][j];
所以取inc=lx[i]+ly[j]-MAX(w[i][j])

很好,看完前面的抽象理解之后你可以考虑一下前置目录下的内容
我说说我的理解
我们要修改顶标,是为什么呢?
这里我解释一下:不成立的话,我们就得换掉一条边,再看看是不是成立
怎么换呢?很容易想到,我们要换一条边要使得减少的量最少,这样可以保证,如果成立,那么一定是最优匹配
这样一来是不是顿时感觉上面都是废话。(这也是我看重思想的一个原因,明白思想的话,写代码其实很容易,而且代码容易忘,思想却是深刻的)
我们要使得减少的量最少所以就取inc=lx[i]+ly[j]-MAX(w[i][j])
关键要明白一点,不成立的情况下,我们要减小值让它成立,而减小值就是要尽量减少的小,让下一个要计算的值尽可能的大

代码

for(int u=0;u<n;u++)//对x里每一个点找匹配
    {
        while(1)
        {
            memset(sx,0,sizeof sx);//每一个点找匹配都要重新判断是否找到过这个点,所以每次都要初始化
            memset(sy,0,sizeof sy);
            if(search_path(u))break;//如果找到匹配就下一个
            int inc=0x7fffffff;
            for(int i=0;i<n;++i)//如果找不到匹配就修改顶标
            {
                if(sx[i])//因为是u找匹配,所以只要修改过程中找到过的点的值就可以了
                {
                    for(int j=0;j<n;++j)
                    {
                        if(!sy[j]&&((lx[i]+ly[j]-w[i][j])<inc))inc=lx[i]+ly[i]-w[i][j];
                    }
                }
            }
            for(int i=0;i<n;++i)
            {
                if(sx[i])lx[i]-=inc;
                if(sy[i])ly[i]+=inc;
            }
        }
    }

求答案

emm,这没啥好说的,前面都明白了就很容易

代码

int sum=0;
    for(int i=0;i<n;++i)
    {
        if(match[i]!=-1)sum+=w[match[i]][i];//因为是完备匹配所有if的条件其实没什么用
    }
    if(!max_weight)sum=-sum;

拓展应用

1.要求最小权完备匹配只需将所有的边权值取其相反数,求最大权完备匹配,匹配的值再取相反数即可。
2.Kuhn-Munkers算法的运行要求是必须存在一个完备匹配,如果求一个最大权匹配(不一定完备)可以把不存在的边权值赋为0。
3.若要边权之积最大则每条边权取自然对数,然后求最大和权匹配,求得的结果a再算出e^a就是最大积匹配。

优化(加入松弛数)

你可以发现前面修改顶标找inc用了for嵌套
其实如果我们把lx[i]+ly[i]-w[i][j];记录,完全可以写成一层for
这就不细讲了

bool search_pp(int u)
{
    int tem;
    sx[u]=true;
    for(int i=0;i<n;++i)
    {
        if(sy[i])continue;
        tem=lx[u]+ly[i]-w[u][i];
        if(tem=0)pp
        {
            sy[i]=true;
            if(match[i]==-1||search_pp(match[i]))
            {
                match[i]=u;
                return true;
            }
        }
        else if(slack[i]>tem)slack[i]=tem;
    }
    return false;
}


for(int u=0;u<n;++u)
{
     for(int j=0;j<n;++j)slack[j]=0x7fffffff;
     while(1)
     {
         memset(sx,false,sizeof sx);
         memset(sy,false,sizeof sy);
         if(search_path(u))break;
         int inc=0x7fffffff;
         for(int j=0;j<n;++j)//因为如果不是找增广路时找到过,那么slack是非常大的,不影响结果
                 if(!sy[j]&&inc>slack[j])inc=slack[j];
         for(int i=0;i<n;++i)
         {
             if(sx[i])lx[i]-=inc;
             if(sy[i])ly[i]+=inc;
         }
     }
 }

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章