二分图匹配例题及拓展

例题

洛谷P1894

分析:

裸题,牛栏作为一个点集,牛作为另一个,牛喜欢牛栏则从牛向牛栏连一条边跑匈牙利就得了,邻接表开大点

代码:

#include<bits/stdc++.h>
#define MAXN (2000+5)
using namespace std;
inline int read(){
    int cnt=0,f=1;char c;
    c=getchar();
    while(!isdigit(c)){
        if(c=='-')f=-f;
        c=getchar();
    }
    while(isdigit(c)){
        cnt=cnt*10+c-'0';
        c=getchar();
    }
    return cnt*f;
}
int n,m,nxt[2*MAXN],first[2*MAXN],to[2*MAXN],match[2*MAXN],x,y,ans,tot,res;
bool vis[2*MAXN];
void add(int x,int y){
    nxt[++tot]=first[x];
    first[x]=tot;
    to[tot]=y;
}
int find(int u){
    for(register int i=first[u];i;i=nxt[i]){
        int v=to[i];
        if(vis[v])continue;
        else {
            vis[v]=true;
            if(match[v]==-1||find(match[v])) {
                match[v]=u;
                match[u]=v;
                return 1;
            }
        }
    }
    return 0;
}
int hungary(){
    for(register int i=1;i<=n;i++){
        for(register int j=1;j<=2*n+1;j++)vis[j]=false;
        if(match[i]==-1)ans+=find(i);
    }
    return ans;
}
int main(){
    n=read();m=read();
    for(register int i=1;i<=n+m;i++)match[i]=-1;
    for(register int i=1;i<=n;i++){
        x=read();
        for(register int j=1;j<=x;j++){
            y=read();add(i,y+n);
        }
    }
        
    printf("%d",hungary());
    return 0;
}

题目链接:

洛谷P1129

分析:

行和列分别作为两边的点,将原图的点作为边建图
因为行和列的交换并不会改变图的形态而只是交换编号,所以对最终答案没有影响
所以读入图的时候如果Map[i,j]==1就从i往j连一条边
然后跑最大匹配就可以了,跑完若ans==n则说明所有行和列都有对应匹配,那么说明有方案,如果ans<n说明不是所有的行和列都能有对应匹配,所以就不能通过变换达成对角线(每行每列都有匹配且不和其他行/列的匹配冲突),说明没有方案
多组数据,记得初始化

代码:

#include<bits/stdc++.h>
#define N (20000+5)
#define M (200000+5)
using namespace std;
inline int read(){
    int cnt=0,f=1;char c;
    c=getchar();
    while(!isdigit(c)){
        if(c=='-')f=-f;
        c=getchar();
    }
    while(isdigit(c)){
        cnt=cnt*10+c-'0';
        c=getchar();
    }
    return cnt*f;
}
int n,t,nxt[M],first[N],to[M],x,match[N],tot,res,ans;
bool vis[N];
void add(int x,int y){
    nxt[++tot]=first[x];
    first[x]=tot;
    to[tot]=y;
}
int find(int u){
    for(register int i=first[u];i;i=nxt[i]){
        int v=to[i];
        if(vis[v]) continue;
        else {
            vis[v]=true;
            if(match[v]==-1||find(match[v])){
                match[v]=u;
                return 1;
            }
        }
    }
    return 0;
}
int hungary(){
    for(register int i=1;i<=n;i++) {
        for(register int j=1;j<=n;j++)vis[j]=false;
        ans+=find(i);
    }
    return ans;
}
int main(){
    t=read();
    while(t--) {
        n=read();
        for(register int i=1;i<=n;i++)match[i]=-1;
        
        for(register int i=1;i<=n;i++)
            for(register int j=1;j<=n;j++) {
                x=read();if(x)add(i,j);
            }
            
        res=hungary();
        if(res==n)printf("Yes\n");
        else printf("No\n");
        for(register int i=1;i<=n;i++) first[i]=0;
        for(register int i=1;i<=tot;i++) nxt[i]=to[i]=0;
        tot=0;res=0;ans=0;
    }
    return 0;
}

另外上面代码的初始化非常猥琐……memset太慢了,所以自己根据n的大小来初始化的不过并没有快多少


题目链接:

POJ2446

分析:

由于每张纸片是\(1*2\)的,并且每张纸片覆盖的两个格子的坐标\((i,j)\)必有\((i+j)\)奇偶性不相同,所以考虑以\((i+j)\)的奇偶性建立两个点集,然后只要没洞/没越界都可以把相邻两个格子代表的点连上一条边,跑出最大匹配\(M\),如果\(M*2\)=\(n*m-k\)则说明有方案,反之则无。

代码:

#include<bits/stdc++.h>
#define N 500
#define M 10000
using namespace std;
inline int read(){
    int cnt=0,f=1;char c;
    c=getchar();
    while(!isdigit(c)){
        if(c=='-')f=-f;
        c=getchar();
    }
    while(isdigit(c)){
        cnt=cnt*10+c-'0';
        c=getchar();
    }
    return cnt*f;
}
int n,m,t,Map[N][N],color[N][N],white=0,black=0,x,y,nxt[M],first[N],to[M],ans,tot,match[N];
bool vis[N];
void add(int x,int y){
    nxt[++tot]=first[x];
    first[x]=tot;
    to[tot]=y;
}
void build_map(){
    /*------------------染色,编号------------------*/ 
    for(register int i=1;i<=n;i++)
        for(register int j=1;j<=m;j++){
            if((i+j)%2)color[i][j]=++white;
            else color[i][j]=++black;
        }
    /*------------------染色,编号------------------*/
    
    /*--------------------连边----------------------*/ 
    for(register int i=1;i<=n;i++)
        for(register int j=1;j<=m;j++)
            if(!Map[i][j]&&(i+j)%2){
                if(i-1>0&&!Map[i-1][j])add(color[i][j],color[i-1][j]);  //上 
                if(j-1>0&&!Map[i][j-1])add(color[i][j],color[i][j-1]);  //左
                if(i+1<=n&&!Map[i+1][j])add(color[i][j],color[i+1][j]); //下 
                if(j+1<=m&&!Map[i][j+1])add(color[i][j],color[i][j+1]); //右
            }
    /*--------------------连边----------------------*/ 
    
}
int find(int u){
    for(register int i=first[u];i;i=nxt[i]){
        int v=to[i];
        if(vis[v])continue;
        else {
            vis[v]=1;
            if(match[v]==-1||find(match[v])){
                match[v]=u;
                return 1;
            }
        }
    }
    return 0;
}
int hungary(){
    for(register int i=1;i<=white;i++){
        for(register int j=1;j<=black;j++)vis[j]=0;
        ans+=find(i);
    }
    return ans;
}
int main(){
    n=read();m=read();t=read();
    for(register int i=1;i<=t;i++) { x=read();y=read();Map[y][x]=1; }   /*这里巨坑……输入的是坐标形式的(x,y)所以要反过来打标记*/ 
    build_map();
    for(register int i=1;i<=black;i++)match[i]=-1;
    int res=hungary();
//  cout<<res;return 0;
    if(res*2==n*m-t)printf("YES");
    else printf("NO");
    return 0;
}

拓展

1 最小点覆盖

最小点覆盖要求用最少的点(任意集合均可)让每条边都至少与一个点相关联。
说人话就是在二分图里面选一些点出来,让每个边都有至少一个端点。
可以证明最小点覆盖=最大匹配\(M\)
简要证明如下:

  • \(M\)个是充分的:将这些点覆盖\(M\)条匹配边,则其他边也一定被覆盖。若有其他边未被覆盖,则将此边加入可得到一个更大的匹配\(M'\),与\(M\)为最大匹配矛盾。
  • \(M\)个是必要的:仅考虑这\(M\)条匹配边,根据最大匹配的定义可以知道,由于这\(M\)条匹配边没有公共点,所以至少需要\(M\)个顶点将其全部覆盖。

例题:

POJ3041 Asteroids

题意:

一个平面内给出一些障碍物,每次操作可以消除整行或整列的障碍物,问最少需要操作多少次?

分析:

这道题可以将行和列转化成二分图里的两个点集,而障碍物作为边的形式存在,若障碍物在\(i\)\(j\)列,则从\(i\)\(j\)连一条边。一次消除一行或一列的操作可以看做是选一个点,而这个点所覆盖的边(即障碍物)将被“清除”,于是问题转化为求二分图的最小点覆盖,最小点覆盖=最大匹配数,跑匈牙利即可。
事实上,有不少问题也采用了类似的转化方式(即若题目所给的一条信息中有两个特征,那么将这两个特征分别作为\(X\)点集和\(Y\)点集中的点,而其信息本身作为边),但做题时切不能生搬硬套。

代码:

#include<bits/stdc++.h>
#define N (40000+5)
#define M (200000+5)
using namespace std;
inline int read(){
    int cnt=0,f=1;char c;
    c=getchar();
    while(!isdigit(c)){
        if(c=='-')f=-f;
        c=getchar();
    }
    while(isdigit(c)){
        cnt=cnt*10+c-'0';
        c=getchar();
    }
    return cnt*f;
}
int n,m,x,y;
int nxt[M],first[N],to[M],match[N],ans,tot;
bool vis[N];
void add(int x,int y){
    nxt[++tot]=first[x];
    first[x]=tot;
    to[tot]=y;
}
int find(int u){
    for(register int i=first[u];i;i=nxt[i]) {
        int v=to[i];
        if(vis[v]) continue;
        else {
            vis[v]=true;
            if(match[v]==-1||find(match[v])) {
                match[v]=u;
                return 1;
            }
        }
    }
    return 0;
}

int hungary() {
    for(register int i=1;i<=m;i++) match[i]=-1;
    for(register int i=1;i<=n;i++){
        for(register int j=1;j<=m;j++) vis[j]=false;
        ans+=find(i);
    }
    
    return ans;
}
int main(){
    n=read();m=read();
    for(register int i=1;i<=m;i++) {
        x=read();y=read();add(x,y);
    }
    printf("%d",hungary());
    return 0;
}

话说这个题为什么初始化\(vis\)数组的时候把\(j\)打成\(i\)了还有64啊……


2 最大独立集

最大独立集:在包含N个点的图G中选出m个点,使得这些点之间两两没有连边,当m取得最大值时称这些选出的点集为图G的最大独立集
对于二分图中的最大独立集有一个重要结论:
二分图中的最大独立集=节点数-最小覆盖点数
简要证明一下:
在一个二分图G中,每一条边中都至少有一个顶点在G的覆盖集中,所以对于每条边上的未选点,其对面的点都在覆盖集中,所以剩下的点两两不会有连边,这就是一个独立集。此时这个独立集与覆盖集互补。于是有最大独立集与最小覆盖点集互补。
有了以上结论,结合“最小覆盖点=最大匹配”可以得到最大独立集=节点数-最大匹配

例题:

1479410-20190124163852984-865588688.jpg
WOJ上的,学校电脑不方便粘网址……

分析:

感觉这个题很毒啊,数据真的保证了男女配对不冲突吗
要是来一组
3 3
1 2
2 3
3 1
那不就GG了吗……1和3明显gay住了啊
然后我就开始手动编号,先无脑连边建个乱七八糟不知道是什么的图再递归染色,写了一个多小时尝试各种姿势建图,然后在WOJ上WA成了傻逼……后来去问了下梁老关于这个题数据的问题,梁老表示这个题数据好像是有点锅,唔那就不管了吧,无脑连边水个AC
没错我在AC面前屈服了

代码:

并没有大锅但是GG了的代码长这样:
#include<bits/stdc++.h>
#define N 10000
#define M 20000
using namespace std;
inline int read(){
    int cnt=0,f=1;char c;
    c=getchar();
    while(!isdigit(c)){
        if(c=='-')f=-f;
        c=getchar();
    }
    while(isdigit(c)){
        cnt=cnt*10+c-'0';
        c=getchar();
    }
    return cnt*f;
}
int n,m,x,y,nxt[M],first[N],to[M],tot,match[N],ans=0,a[N],b[N];
int belongs[N];
bool vis[N],built[N]; 
void add(int x,int y){
    nxt[++tot]=first[x];
    first[x]=tot;
    to[tot]=y;
}
void build_map(int u){
    built[u]=1;
    if(belongs[u]==-1)belongs[u]=1;
    for(register int i=first[u];i;i=nxt[i]){
        int v=to[i];
        belongs[v]=1-belongs[u];
//      cout<<"v="<<v<<" belongsto"<<belongs[v]<<endl;
        build_map(v);
    }
    return;
}
int find(int u){
    for(register int i=first[u];i;i=nxt[i]){
        int v=to[i];
        if(vis[v]) continue;
        else {
            vis[v]=true;
            if(match[v]==-1||find(match[v])){
                match[v]=u;
                return 1;
            }
        }
    }
    return 0;
}
int hungary(){
    for(register int i=1;i<=n;i++)match[i]=-1;
    for(register int i=1;i<=n;i++){
        for(register int j=1;j<=n;j++) vis[j]=false;
        if(belongs[i]==1)ans+=find(i);
    }
    return ans;
}
int main(){
    n=read();m=read();
    for(register int i=1;i<=n;i++) belongs[i]=-1,built[i]=0; 
    for(register int i=1;i<=m;i++){
        a[i]=read();b[i]=read();
        a[i]++;b[i]++;add(a[i],b[i]);
    }
//  belongs[a[1]]=1;
//  for(register int i=1;i<=m;i++)cout<<"a[i]="<<a[i]<<" b[i]="<<b[i]<<endl;
    for(register int i=1;i<=m;i++)
        if(!built[a[i]]) build_map(a[i]);
    
    for(register int i=1;i<=tot;i++)nxt[i]=to[i]=0;
    for(register int i=1;i<=n;i++)first[i]=0;
    tot=0;
    for(register int i=1;i<=n;i++){
        if(belongs[a[i]]==1)add(a[i],b[i]);
        if(belongs[b[i]]==1)add(b[i],a[i]);
    }
//  for(register int i=1;i<=n;i++)cout<<belongs[i];
    printf("%d",n-hungary());
    return 0;
}
AC代码,不要把这玩意当成上面例题的题解,当个求最大独立集板子用……
#include<bits/stdc++.h>
#define N 10000
#define M 20000
using namespace std;
inline int read(){
    int cnt=0,f=1;char c;
    c=getchar();
    while(!isdigit(c)){
        if(c=='-')f=-f;
        c=getchar();
    }
    while(isdigit(c)){
        cnt=cnt*10+c-'0';
        c=getchar();
    }
    return cnt*f;
}
int n,m,x,y,nxt[M],first[N],to[M],tot,match[N],ans=0;
bool vis[N],built[N]; 
void add(int x,int y){
    nxt[++tot]=first[x];
    first[x]=tot;
    to[tot]=y;
}

int find(int u){
    for(register int i=first[u];i;i=nxt[i]){
        int v=to[i];
        if(vis[v]) continue;
        else {
            vis[v]=true;
            if(match[v]==-1||find(match[v])){
                match[v]=u;
                return 1;
            }
        }
    }
    return 0;
}
int hungary(){
    for(register int i=1;i<=n;i++)match[i]=-1;
    for(register int i=1;i<=n;i++){
        for(register int j=1;j<=n;j++) vis[j]=false;
        ans+=find(i);
    }
    return ans;
}
int main(){
    n=read();m=read();
    for(register int i=1;i<=m;i++){
        x=read();y=read();
        x++;y++;add(x,y);
    }
//  for(register int i=1;i<=n;i++)cout<<belongs[i];
    printf("%d",n-hungary());
    return 0;
}

3 最小边覆盖

边覆盖集:在图G中选出一些边,构成的能使得G中所有顶点都在某条边上的边集。
最小边覆盖:所有的边覆盖集中最小的一个集合。
最小边覆盖=最大独立集=n-最大匹配

转载于:https://www.cnblogs.com/kma093/p/10312475.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值