二分图最大匹配

1️⃣增广路径

1.概念:

若路径P是图G中一条连通两个顶点,这两个顶点与M中无匹配关系(即这两个点都是非匹配点)的路径,且属于M的边和不属于M的边在P上交替出现,则称P为相对于M的一条增广路径

2.性质

在这里插入图片描述
图中的增广路径P为12345

  • 根据概念可知,处于P两端的边一定不属于M,且P长度肯定是奇数的

属于M的边有2和4,匹配数量为2,其补图有边1,3,5,匹配数量为3

  • 增广路径取反后必然能得到一个更大的匹配
  • M为最大匹配当且仅当增广路径P不存在

这就提供了一个求二分图最大匹配的一个思路:

如果增广路径的长度不断变大,就相当于M的匹配也在变大!




2️⃣匈牙利算法解决二分图最大匹配

(注意:点不能重复使用,故需要用一个数组进行标记)
图1
一开始,M中没有匹配关系,随意取一条 x 1 y 1 x_1y_1 x1y1
然后对于 x 2 x_2 x2,也可以取 x 2 y 2 x_2y_2 x2y2,M变成下图的匹配关系
在这里插入图片描述
但是当 x 3 x_3 x3想要和 y 1 y_1 y1匹配的时候发现 y 1 y_1 y1已经被占用了
就把 y 1 y_1 y1的原主 x 1 x_1 x1暴揍一顿赶走了
x 1 x_1 x1很不爽就去找"第二归宿" y 2 y_2 y2,结果还是 x 2 x_2 x2占用着
就把 x 2 x_2 x2揍了一顿,于是 x 2 x_2 x2只能去找 y 5 y_5 y5
这是一个递归揍人的过程,揍完之后的匹配图如下:
在这里插入图片描述
路径: x 3 − y 1 − x 1 − y 2 − x 2 − y 5 x_3-y_1-x_1-y_2-x_2-y_5 x3y1x1y2x2y5
在图二中对照发现正好是一条增广路径

取反之后得到图三,此时的匹配状态由于构造出了增广路径,故匹配数量变大,由2变成了3

匈牙利算法的算法核心就是通过构造增广路径来扩大匹配。

至于这个构造的过程就是递归揍人的过程,反正我不懂是如何实现的,不深究




3️⃣例题

①51Nod 2006
直接对两部分进行匹配

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>
#include<string>
#include<vector>
#include<stack>
#include<bitset>
#include<cstdlib>
#include<cmath>
#include<set>
#include<list>
#include<deque>
#include<queue>
#include<map>
#define ll long long
#define pb push_back
#define rep(x,a,b) for (int x=a;x<=b;x++)
#define repp(x,a,b) for (int x=a;x<b;x++)
#define W(x) printf("%d\n",x)
#define mem(a,x) memset(a,x,sizeof a)
using namespace std;
const int maxn=1e3+7;
int mapp[maxn][maxn];
int linker[maxn];
bool vis[maxn];
int ans,n,m,a,b;
bool dfs(int u)
{
    for (int i=m+1;i<=n;i++)
    {
        if (mapp[u][i]&&!vis[i])
        {
            vis[i]=true;
            if (linker[i]==-1||dfs(linker[i]))
            {
                linker[i]=u;
                return true;
            }
        }
    }
    return false;
}
void init()
{
    mem(linker,-1);
    ans=0;
}
int count_()
{
    for (int i=1;i<=m;i++)
    {
        mem(vis,false);
        if (dfs(i))ans++;
    }
    return ans;
}
int main()
{
    cin>>m>>n;
    init();
    while(scanf("%d%d",&a,&b)&&!(a==-1&&b==-1))
    {
        mapp[a][b]=1;
    }
    int x=count_();
    if (x==0)cout<<"No Solution!"<<endl;
    else cout<<x<<endl;
    return 0;
}

②洛谷P3386(模板题)
代码略
https://www.luogu.com.cn/problemnew/show/P3386

③PKUOJ1469(模板题)
代码略
http://poj.org/problem?id=1469

④洛谷P1640
https://www.luogu.com.cn/problem/P1640
属性值当作一个点,武器当作一个点,一个武器对应连两个属性值的边
两部分进行二分匹配
不太一样的是:选择第i+1属性值的前提是选择第i属性值,那么在匈牙利算法时,如果dfs(i)失败时,即无法将第i个属性值纳入增广路径,就break退出即可
本题由于数据范围,不能使用邻接矩阵存储,改写邻接矩阵

const int maxn=2e6+7;
int linker[maxn],n,a,b;
bool vis[maxn];
struct Edge
{
    int to,next;
}edge[maxn];
int head[maxn],tot;
void init()
{
    tot=0;
    mem(head,-1);
    mem(linker,-1);
}
void add(int u,int v)
{
    edge[tot].to=v;
    edge[tot].next=head[u];
    head[u]=tot++;
}
bool dfs(int u)
{
    for (int i=head[u];i!=-1;i=edge[i].next)
    {
        int v=edge[i].to;
        if (!vis[v])
        {
            vis[v]=true;
            if (linker[v]==-1||dfs(linker[v]))
            {
                linker[v]=u;
                return true;
            }
        }
    }
    return false;
}
int count_()
{
    int ans=0;
    for (int i=1;i<=10000;i++)
    {
        mem(vis,false);
        if (dfs(i))ans++;
        else break;
    }
    return ans;
}
int main()
{
    cin>>n;
    init();
    rep(i,1,n)
    {
        scanf("%d%d",&a,&b);
        add(a,i);
        add(b,i);
    }
    W(count_());
    return 0;
}

⑤洛谷P1129
https://www.luogu.com.cn/problem/P1129
将矩阵的行[1-n]和矩阵的列[1-n]作为左右二部分
可以证明的是当最大匹配达到n的时候
通过几次行变换就可以实现对角线全是1

const int maxn=2e2+7;
bool vis[maxn];
int mapp[maxn][maxn];
int linker[maxn],n,x,T;
bool dfs(int u)
{
    for (int i=1;i<=n;i++)
    {
        if (mapp[u][i]&&!vis[i])
        {
            vis[i]=true;
            if (linker[i]==-1||dfs(linker[i]))
            {
                linker[i]=u;
                return true;
            }
        }
    }
    return false;
}
int count_()
{
    int ans=0;
    for (int i=1;i<=n;i++)
    {
        mem(vis,false);
        if (dfs(i))ans++;
    }
    return ans;
}
int main()
{
    scanf("%d",&T);
    while(T--)
    {
        mem(linker,-1);
        mem(mapp,0);
        scanf("%d",&n);
        rep(i,1,n)
        {
            rep(j,1,n)
            {
                scanf("%d",&x);
                if (x==1)mapp[i][j]=1;
            }
        }
        if (count_()==n)cout<<"Yes"<<endl;
        else cout<<"No"<<endl;
    }
    return 0;
}

⑥洛谷1963
https://www.luogu.com.cn/problem/P1963

左边为[0~n-1]的排列,右边为每一个数字衍生出的两个可能性,用最大匹配解即可
如果是从正向建立增广路径,那么会后面的会不断的覆盖前面的最优解,那么正确的做法就是从后到前,这时候记录的也就不是linker,而是作为反函数的inv
还有一个坑点就是这两个衍生数字的大小问题,应该是优先选择小的,所以小的在遍历过程中应该处于前面点的位置。

const int maxn=2e6+7;
int tot,linker[maxn],n,a[maxn],inv[maxn];
bool vis[maxn];
int mapp[maxn][2];
bool dfs(int u)
{
    for (int i=0;i<2;i++)
    {
        int v=mapp[u][i];
        if (!vis[v])
        {
            vis[v]=true;
            if (linker[v]==-1||dfs(linker[v]))
            {
                linker[v]=u;
                inv[u]=v;
                return true;
            }
        }
    }
    return false;
}
int count_()
{
    int ans=0;
    for (int i=n-1;i>=0;i--)
    {
        mem(vis,false);
        if (dfs(i))ans++;
    }
    return ans;
}
int main()
{
    scanf("%d",&n);
    mem(linker,-1);
    rep(i,0,n-1)
    {
        scanf("%d",&a[i]);
        int p=(i+a[i])%n;
        int q=(i-a[i]+n)%n;
        if (p>q)swap(p,q);
        mapp[i][0]=p;mapp[i][1]=q;
    }
    int x=count_();
    if (x<n)cout<<"No Answer"<<endl;
    else
    {
        rep(i,0,n-1){cout<<inv[i];if (i!=n-1)cout<<" ";}cout<<endl;
    }
    return 0;
}

⑦洛谷P3231

https://blog.csdn.net/w_udixixi/article/details/103982191

⑧HDU1281
棋盘问题直接最大匹配处理,但是“重要的点”不能一眼看出,就先把它去掉,再算一次最大匹配,如果小于原值,就说明是重要的点
算法过于暴力,时间够

const int maxn=2e2+7;
const int INF=1e9;
const ll INFF=1e18;
int mapp[maxn][maxn],x[maxn],y[maxn];
bool vis[maxn];
int linker[maxn];
int n,m,k,cnt=1;
bool dfs(int u)
{
    for (int i=1;i<=m;i++)
    {
        if (mapp[u][i]&&!vis[i])
        {
            vis[i]=true;
            if (linker[i]==-1||dfs(linker[i]))
            {
                linker[i]=u;
                return true;
            }
        }
    }
    return false;
}
int count_()
{
    int ans=0;
    for (int i=1;i<=n;i++)
    {
        mem(vis,false);
        if (dfs(i))ans++;
    }
    return ans;
}
int main()
{
    while(~scanf("%d%d%d",&n,&m,&k))
    {
        mem(linker,-1);
        mem(mapp,0);
        rep(i,1,k)
        {
            scanf("%d%d",&x[i],&y[i]);
            mapp[x[i]][y[i]]=1;
        }
        int X=count_(),num=0;
        for (int i=1;i<=k;i++)
        {
            mem(linker,-1);
            for (int j=1;j<=k;j++)
            {
                if (j==i)mapp[x[i]][y[i]]=0;
            }
            int Y=count_();
            if (Y<X)num++;
            mapp[x[i]][y[i]]=1;
        }
        printf("Board %d have %d important blanks for %d chessmen.\n",cnt++,num,X);
    }
}





4️⃣Hopcroft-Karp算法

复杂度: O ( E ∗ V ) O(E*\sqrt{V}) O(EV )

最大匹配建图是核心,那么这个算法就不研究了。

一般一提到最大匹配就会想到匈牙利算法,但是这种的算法复杂度确实是要小一点的,没遇到过具体的题目所以不知所用

下面放一道模板题:除了vector存图其余全是板子

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>
#include<string>
#include<vector>
#include<stack>
#include<bitset>
#include<cstdlib>
#include<cmath>
#include<set>
#include<list>
#include<deque>
#include<queue>
#include<map>
#define ll long long
#define pb push_back
#define rep(x,a,b) for (int x=a;x<=b;x++)
#define repp(x,a,b) for (int x=a;x<b;x++)
#define W(x) printf("%d\n",x)
#define mem(a,x) memset(a,x,sizeof a)
using namespace std;
const int maxn=2e3+7;
const int INF=1e9;
vector<int> G[maxn];
int Mx[maxn],My[maxn];
int dx[maxn],dy[maxn];
int dis,p,n;
bool vis[maxn];
bool bfs()
{
    queue<int> Q;
    dis=INF;
    mem(dx,-1);mem(dy,-1);
    for (int i=1;i<=p;i++)
    {
        if (Mx[i]==-1)
        {
            Q.push(i);
            dx[i]=0;
        }
    }
    while(!Q.empty())
    {
        int u=Q.front();
        Q.pop();
        if (dx[u]>dis)break;
        repp(i,0,G[u].size())
        {
            int v=G[u][i];
            if (dy[v]==-1)
            {
                dy[v]=dx[u]+1;
                if (My[v]==-1)dis=dy[v];
                else
                {
                    dx[My[v]]=dy[v]+1;
                    Q.push(My[v]);
                }
            }
        }
    }
    return dis!=INF;
}
bool dfs(int u)
{
    repp(i,0,G[u].size())
    {
        int v=G[u][i];
        if (!vis[v]&&dy[v]==dx[u]+1)
        {
            vis[v]=true;
            if (My[v]!=-1&&dy[v]==dis)continue;
            if (My[v]==-1||dfs(My[v]))
            {
                My[v]=u;
                Mx[u]=v;
                return true;
            }
        }
    }
    return false;
}
int count_()
{
    int res=0;
    mem(Mx,-1);
    mem(My,-1);
    while(bfs())
    {
        mem(vis,false);
        for (int i=1;i<=p;i++)if (Mx[i]==-1&&dfs(i))res++;
    }
    return res;
}
int main()
{
    int t,a,b;
    scanf("%d",&t);
    while(t--)
    {
        scanf("%d%d",&p,&n);
        rep(i,1,p)G[i].clear();
        rep(i,1,p)
        {
            scanf("%d",&a);
            repp(j,0,a)
            {
                scanf("%d",&b);
                G[i].pb(b);//vector存图-
                //这是一道模板题,最基本的建图
            }
        }
        int x=count_();
        if (x==p)cout<<"YES"<<endl;
        else cout<<"NO"<<endl;
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值