二分匹配专辑

好久没写博客了,中间做过dp搜索图论的其他知识,也遇到了一些好题,但是都没有写下来,有些可惜----

bool dfs(int a,bool flg)
{
    for(int i=1;i<=m;i++)
    {
        if(mp[a][i]&&!vist[i])
        {
            vist[i]=true;
            if(-1==yem[i]||dfs(yem[i],flg))
            {
                if(flg)
                {
                    yem[i]=a;
                    xem[a]=i;
                }
                return true;
            }
        }
    }
    return false;
}

  

这是二分匹配的模版,当然建议这样写。这里我用的是x集合去匹配y几何,这样
xem数组装的就是a这个店匹配到的i值.......这样写的好处是,一遍dfs,就把两边
相互匹配的点都找出来了,方便下面的操作。
最大匹配,说白了,就是一个几何对另一集合在一对一匹配的情况下,所能匹配的
最大数......

题目出法:
1、赤裸裸的求最大匹配、最小顶点覆盖的题目,此类题目应该说是水题
hdu 1068 1150 1151 1179 就是这样的题目了
先把两个几何确定好,然后依照已有的关系建图
例如1150
有两台机器,还有n个任务在这两台机器上完成任务所需要的模式,现在要求机器的
最少关机次数(这题有个坑 的地方,就是模式0是机器的初始模式)
事实上,建图的时候就是同一个任务在机器1与机器2的不同模式的最大匹配。
例如1068
虽然可以知道男孩、女孩必定是两个集合,但是仅凭借关系无法推知哪个是男孩
哪个是女孩。
这样的话,我们可以把“复制”一份人,使得原有的n个人变成2*n个人,再去建图,
求最大独立集,将求出来的结果/2 就是答案


2:行列匹配:
这类题目做过几个,但是都找不到原题了--
hdu 2119
行列匹配最简单的表达方式就是一行或者一列最多只能放一个元素,或者说最多选择
一个元素,下面会有行列匹配与二分匹配其他用法相结合的题目。
关于hdu2119这个题目,它是说你每次操作可以删除一行或者一列的1,问你在一个矩
阵内将它给出的1全部删掉所需要的最少次数--
实际上,只要一行或者一列出现一个或者多个1,那么我们就需要对他们进行连接
然后就是求最大匹配了。


3:最大独立集:
这类题目,只要你找好关系,那么就是水题,反之就是难题。
说说我遇到的几个有意思的独立集:
hdu 2768 3517
我觉得要是你认为一个题目可以用二分匹配解决,那么第一步肯定是分集合,第二步
是找关系,这里的关系可以是顺着题意的关系,也可以是逆着题意的关系。我所定义
的所谓逆着题意的关系,就是我按照两个集合产生矛盾的相关点来建立二分图
比如说 hdu 2768这个题目
它是说,一个人会喜欢一只猫cati,并且讨厌一只狗dogj或者喜欢一只狗dogi,并且讨
厌一只猫catj,现在有猫m只,狗n只,人k个,给出所有人喜欢猫狗的信息,要你求最多
的人可以呆在同一个集合,(假如一个人喜欢cat1,讨厌dog1,那么其他所有喜欢dog1
或者讨厌cat1的人不能和这个人呆在同一个集合)

比较明显的独立集题目,但是关系该怎么找呢?
这里有一个我觉得有用的方法,就是假如独立集要求a,就建立a与a之间的关系
再求独立集。
这里是要求最多剩下的人,那么就建立人与人之间的关系,很快可以发现,无法
顺着题意给出的信息来建立人与人之间的关系。那么,其实是可以建立我上面所说
的逆着的关系,就是人与人产生矛盾,那么我们就建立这两个人的关系。
至于二分图,很明显,喜欢猫的人和喜欢狗的人是不同的两个集合。
那么,就可以依照这样的信息来建立二分图:
假如一个人所喜欢的被另一个所讨厌或者一个人所讨厌的被另一个人所喜欢
那么这两个人是可以建立关系的--
建立好二分图,就跑一次最大匹配,然后用顶点个数-最大匹配。
这个代码。是我复制了一次人,也就是使得n个人变成了2*n个人--我第一个代码
是没有复制人的,只是想试下复制人会不会超时--
代码:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
struct node
{
    char ch1[10];
    char ch2[10];
}s[600];
int mp[600][600],flg[600],n,m,k;
bool vist[600];

bool dfs(int a)
{
    for(int i=1;i<=k;i++)
    {
        if(!vist[i]&&mp[a][i])
        {
            vist[i]=true;
            if(-1==flg[i]||dfs(flg[i]))
            {
                flg[i]=a;
                return true;
            }
        }
    }
    return false;
}
int main()
{
    int text;
    scanf("%d",&text);
    while(text--)
    {
        scanf("%d%d%d",&n,&m,&k);
        memset(mp,0,sizeof(mp));
        memset(flg,-1,sizeof(flg));
        for(int i=1;i<=k;i++)
        {
            scanf("%s %s",s[i].ch1,s[i].ch2);
        }
        for(int i=1;i<=k;i++)
        {
            for(int j=1;j<=k;j++)
            {
                if(0==strcmp(s[i].ch1,s[j].ch2)||0==strcmp(s[i].ch2,s[j].ch1))
                {
                    mp[i][j]=1;
                }
            }
        }
        int ans=0;
        for(int i=1;i<=k;i++)
        {
            memset(vist,false,sizeof(vist));
            if(dfs(i))
            {
                ans++;
            }
        }
        printf("%d
",k-ans/2);
    }
    return 0;
}

  

接下来是hdu3517:
题意:有a个a议会的人,b个b议会的人,以及m个提案,每个人会赞同一个提案
反对k个提案,同一议会的人不会反对自己议会的人的提案,如果某个人赞同的
提案被采纳了,并且他所反对的提案都没被采纳,这个人就会很happy(不觉得
猥琐么?这么反人类),问你最多有多少人happy,然后问你哪些提案肯定会通过

很抱歉,这道题目,身为渣渣的我并未a掉-.-
因为它除了求最大独立集,还要求这个独立集里面的必须点---
这个题目我做了将近3天,还是未能ac,不得不吐槽下,要是有朋友
ac了,请一定指点我一下--
我的思路:建立好二分图,求出独立集,然后求出二分图里面的必须点,
所有与二分图的必须点矛盾的点必然是独立集里面的必须点,然后就是二分图
里面的非必须要是没有矛盾的点肯定也会是独立集里面的必须点--

这里说下独立集
题目是问最多有多少人happy?那么肯定是人与人之间的关系--按照矛盾来就好

4、关键点、关键边
额,由于时间的问题,二分匹配我暂时只做了一道关键点的题目,关键边的,接下
来会做吧?--

关键点的定义:在求出每一个点各自匹配的关系之后,将某个点与之匹配的点的这
条边去掉,然后再找一条增广路,如果可以找到一条增广路,那么不是关键点,
反之则是。

hdu 1281就是求关键点的题目

题意:在一个N*M大小的棋盘中,有K个空位置,在这些空位置上最多能放多少的“车”(一行或一列最多一个)。空位置中,有的位置若不放“车”,就无法保证放尽量多的“车”,这样的格子被称做重要点,求重要点的个数

思路:行与列的二分匹配问题,因为每行每列至多只能放一个棋子。第i行与j列匹配代表棋盘第i行j列这个位置放棋子。那么,棋盘上的点就是二分图的边;“车”的个数就是二分图的最大匹配数。题目的关键是求重要点。

1 :枚举所有可以放的点,去掉某一点后(这里的点指棋盘上的点,也就是二分图的边),就得到一个新的二分图了
   if  (新二分图的最大匹配数 == ans)

                 then 这个点不是重要点
   else // 即新的二分图达不到ans这个匹配数,那么这个点就是必须放的,否则达不到ans。 -->重要边
            then 计数+1
2 : 但是这样枚举效率太低。实际上,删边只需考虑求出的匹配边(因为删除非匹配边得到的匹配数不变)。这样,只需删除ans条边,复杂度就降低了。
    再进一步分析,删除一条边以后,没有必要重新求删边后新的二分图的最大匹配,只需检查删边后的匹配中--->可不可以再找到新的增广链就可以了。这样,时间复杂度就进一步降到了。
3 : 这样的优化是不可取的:
    在判断是否存在增广路得时候,不能只以删除的匹配边的顶点作起点来找增广路
    正确的方法是:以删边后新的二分图的所有未匹配顶点出发做增广,都找不到增广路,匹配不能再增加

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
bool mp[105][105],vist[105];
int xem[105],yem[105];
int n,m,k;
//bool flg;

bool dfs(int a,bool flg)  //求最大匹配
{
    for(int i=1;i<=m;i++)
    {
        if(mp[a][i]&&!vist[i])
        {
            vist[i]=true;
            if(-1==yem[i]||dfs(yem[i],flg))
            {
                if(flg)
                {
                    yem[i]=a;
                    xem[a]=i;
                }
                return true;
            }
        }
    }
    return false;
}
bool deal()   //找增广路
{
    for(int i=1;i<=n;i++)
    {
        memset(vist,false,sizeof(vist));
        if(xem[i]==-1)
        {
            if(dfs(i,false))
            return true;
        }

    }
    return false;
}
int main()
{
    int text=1;
    while(scanf("%d%d%d",&n,&m,&k)>0)
    {
        memset(mp,false,sizeof(mp));
        memset(xem,-1,sizeof(xem));
        memset(yem,-1,sizeof(yem));
        for(int i=1;i<=k;i++)
        {
            int a,b;
            scanf("%d%d",&a,&b);
            mp[a][b]=true;
        }
        int ans=0;
        for(int i=1;i<=n;i++)
        {
            if(xem[i]==-1)
            {
                memset(vist,false,sizeof(vist));
                if(dfs(i,true))
                ans++;
            }
        }
        int sum=0;
        for(int i=1;i<=n;i++)
        {

            if(xem[i]!=-1)
            {
                int b=xem[i];    //删掉这条匹配的边
                xem[i]=-1;
                yem[b]=-1;
                mp[i][b]=false;
                if(!deal()) sum++;
                xem[i]=b;       //恢复这条边
                yem[b]=i;
                mp[i][b]=true;
            }
        }
        printf("Board %d have %d important blanks for %d chessmen.
",text++,sum,ans);
    }
    return 0;
}

  

5、二分+二分匹配

hdu 2413

 大意:地球人要占领喵星人的星球,给定地球战舰的初始值以及每年递增的速率,喵星人战舰初始值,以及每年递增的速率,求在一个最小的时间,在改时间内占领全部星球。(一个星球只能占领一个喵星球)

思路:从最后那句话可以看出这绝壁是二分匹配,那就建图吧,然后枚举时间就ok--当然你可能会wa,要注意二分数据的左右范围--

hdu 2236(中文题)

思路:首先,一看就可以知道是行列匹配,然后要求一个最大值减去最小值最小?那么我们现在不限制最大最小值的情况下求出最大匹配k,其实k==n

然后枚举一个最大值,一个最小值,排除在这之外的边,再去进行最大匹配找==n的差值,然后取最小差值。

然后恭喜你,TLE-----好吧,不明白这个题目,这里都要卡时间,硬是要套个二分么?

它的二分就是二分一个差值m,然后利用这个差值去找范围(minx--minx+m),在根据范围求最大匹配--

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
int num[103][103],yem[103],n,minx,maxn;
bool vist[103],mp[103][103];
bool pp[103];
bool dfs(int x)
{
    for(int i=1; i<=n; i++)
    {
        if(!vist[i]&&mp[x][i])
        {
            vist[i]=true;
            if(-1==yem[i]||dfs(yem[i]))
            {
                //if(flg)
                {
                    yem[i]=x;
                    //xem[x]=i;
                }
                return true;
            }
        }
    }
    return false;
}
bool deal(int m)
{
    int i,j,k,ans;
    for( i=minx; i+m<=maxn; i++)
    {
        if(!pp[i]) continue;
        //memset(mp,false,sizeof(mp));
        //memset(xem,-1,sizeof(xem));

        for( j=1; j<=n; j++)
        {
            for( k=1; k<=n; k++)
            {
                if(num[j][k]>=i&&num[j][k]<=i+m)
                {
                    mp[j][k]=true;
                }
                else
                {
                    mp[j][k]=false;
                }
            }

        }
        ans=0;
        memset(yem,-1,sizeof(yem));
        for( j=1; j<=n; j++)
        {
            memset(vist,false,sizeof(vist));
            if(dfs(j))
                ans++;
            if(ans==n)  return true;
        }

    }
    return false;
}
int main()
{
    int text;
    scanf("%d",&text);
    while(text--)
    {
        scanf("%d",&n);
        minx=1000000,maxn=-1;
        //memset(num,-1,sizeof(num));
        memset(pp,false,sizeof(pp));
        for(int i=1; i<=n; i++)
        {
            for(int j=1; j<=n; j++)
            {
                int tmp;
                scanf("%d",&tmp);
                num[i][j]=tmp;
                pp[tmp]=true;
                if(tmp<minx) minx=tmp;
                if(tmp>maxn) maxn=tmp;
            }
        }
        // int a=minx,b=maxn;
        int left=0,right=maxn-minx+1,k=right,mid;
        while(left<=right)   //好吧,不得不承认,我又学到了二分的一个用法
        {
            mid=(left+right)/2;
            if(deal(mid))
            {
                //if(k>mid)
                k=mid;
                right=mid-1;
            }
            else left=mid+1;
        }
        printf("%d
",k);
    }
    return 0;
}

  

写到这里,二分匹配算是写完了--其实我还有许多题目并没有做,有空的时候,还是应该是做做,关键点和关键边的题目的......

原文地址:https://www.cnblogs.com/ziyi--caolu/p/3880484.html