二分图博弈学习笔记

文章介绍了二分图博弈的概念和性质,特别是与最大匹配的关系。当先手状态在最大匹配中时,先手必胜;否则,先手必败。通过网络流算法可以判断一个点是否在最大匹配中,从而确定博弈的胜负。文中还提供了多个例题,展示了如何利用这些理论解决实际问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言:最近每场训练赛都有博弈题,而且我都被薄纱了。。。真烦

二分图博弈是少有的直接跟图论挂钩的一种博弈模型

一个博弈是二分图博弈应当满足一下条件:

  • 博弈人数为两人,轮流操作

  • 博弈状态转移可以表示成一张二分图

  • 不可访问已经访问过的状态

  • 无法转移者负

拥有二分图这么良好的性质,该博弈模型自然而然的有一个很简单的结论:如果先手状态,我们记为P,一定在二分图的最大匹配中,那么先手必胜。否则先手必败。

那么我们来看看证明:

 假设当前P不在最大匹配中,那么先手移动之后P一定会到达一个匹配点,否则我们就有了一个新的匹配,与最大匹配这一点矛盾。所以此时我们的局面是当前处于匹配点上,后手先行动。后手走的下一个点也一定是匹配点,否则就跟之前的路径形成了一条增广路,同样矛盾。所以接下来双方都是在匹配点上转移。显然最终移动步数为偶数,故此时后手必胜。

若P在最大匹配中,则只要仿照上述策略,就是一个必胜的局面。

如果P没有可以转移的局面,同样也是不在最大匹配中,当然也是必败。


由此,对于一个二分图博弈,只要我们能够判断先手所在局面是否处在二分图的最大匹配上,即可判断必胜状态。

如何判断一个点是否一定在最大匹配上?比较经典的套路就是先将其从图中移除,然后再加进来,看看是否还能对匹配有贡献。如果有的话,说明其一定处在最大匹配上。

具体实现,我们可以采用网络流。在建图的时候先不将P与源点或者汇点连接,这样跑网络流的时候就不会将其考虑进去。然后把P与源点/汇点连接,利用残量网络,再做一遍网络流。如果值非0,就是有贡献的。

但是该方法仅适用于单起点博弈 。如果需要判断的局面过多,显然时间复杂度无法接受。

如果想要找出所有局面中的必胜局面,我们有更好的办法。

找出所有必胜局面,也就是找出所有一定处在最大匹配的点,相当于找出所有不一定在最大匹配上的点。

非匹配边1->5和2->5匹配边可以互换最大匹配数不变,所以点1,2都不一定在最大匹配中。在该图的残量网络中,1->5的流量为1,匹配边5->2的反向流量为1,因此我们判断两条边可以互换。将与源点相连的边颜色col设为1,汇点设为0,dfs即可。这是判断与左部点的,判断右部点同理。

但是,但是,并不是所有满足二分图博弈模型的题目都一定得通过网络流来求解,因为不同博弈模型本身有其独特的性质,可能可以通过其他途径求解,比如23牛客多校2 I Link with Gomoku

然后就到了喜闻乐见的例题时间

[COCI2017-2018#5] Planinarenje

大意:

模板题 

不难发现,判负的条件与一方无法行动是等价的,所以该问题完美符合二分图博弈模型。然后又因为要对所有先手局面判断是否必胜,那么我们应该采用第二种方法。

code

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define endl '\n'
const ll N=1e5+10;
const ll maxn=2e5+10,maxm=2e5+10;
const ll inf=0x3f3f3f3f;
struct MaximumFlow
{
    //maxn 最多点数 maxm 最多边数
    int h[maxn],e[maxm],ne[maxm],f[maxm],idx;
    int d[maxn],cur[maxn];
    int n,m,S,T,mxn;
    ll col[maxn],ans[maxn];
    ll flag=0;
    void init(int n,int _s,int _t)
    {
        mxn=n,idx=0,S=_s,T=_t;//mxn表示最大点的大小(包括S,T),用于初始化处理,_s表示源点,_T表示汇点
        // memset(h,-1,sizeof h);
        while(n>=0)
            h[n--]=-1;
    }
    void addedge(int a,int b,int c)
    {
        ne[idx]=h[a],e[idx]=b,f[idx]=c,h[a]=idx++;
        ne[idx]=h[b],e[idx]=a,f[idx]=0,h[b]=idx++;
    }
    bool bfs()
    {
        // memset(d,-1,sizeof d);
        for(int i=0;i<=mxn;++i) d[i]=-1;
        d[S]=0;
        queue<int>q;
        q.push(S);
        cur[S]=h[S];
        while(q.size())
        {
            int t=q.front();
            q.pop();
            for(int i=h[t];i!=-1;i=ne[i])
            {
                int j=e[i];
                if(d[j]==-1&&f[i])
                {
                    d[j]=d[t]+1;
                    cur[j]=h[j];
                    if(j==T)return true;
                    q.push(j);
                }
            }
        }
        return false;
    }
    int find(int u,int limit)
    {
        if(u==T)return limit;
        int flow=0;
        for(int i=cur[u];i!=-1&&flow<limit;i=ne[i])
        {
            cur[u]=i;
            int j=e[i];
            if(d[j]==d[u]+1&&f[i])
            {
                int t=find(j,min(f[i],limit-flow));
                if(!t)d[j]=-1;
                f[i]-=t,f[i^1]+=t,flow+=t;
            }
        }
        return flow;
    }
    int dinic()
    {
        int r=0,flow;
        while(bfs())while(flow=find(S,inf))r+=flow;
        return r;
    }
    void dfs(int u,int t)
    {
        // vis[u]=1;
        if(col[u]==t)
        {
            flag=1;
            ans[u]=1;
        }
        for(int i=h[u];i!=-1;i=ne[i])
        {
            int j=e[i];
            if(f[i]==t&&!ans[j])
            {
                dfs(j,t);
            }
        }
    }
    ll judge()
    {
        dinic();
        dfs(S,1);dfs(T,0);
        return flag;
    }

}f;
ll n,m;
ll S,T;
ll a,b;
void solve()
{
    cin>>n>>m;
    S=n*2+1;T=n*2+2;
    f.init(T+1,S,T);
    while(m--)
    {
        cin>>a>>b;
        f.addedge(a,b+n,1);
    }
    for(int i=1;i<=n;++i)
    {
        f.addedge(S,i,1);
        f.col[i]=1;
        f.addedge(i+n,T,1);
    }
    f.judge();
    for(int i=1;i<=n;++i)
    {
        if(f.ans[i]==0) cout<<"Slavko"<<endl;
        else cout<<"Mirko"<<endl;
    }

}
int main()
{
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    solve();
    return 0;
}

20ccpc长春H

大意:
有m个数位的数字锁,存在若干状态不允许访问,也不允许访问之前访问过的节点,每次操作改变某一位,无法操作者负。给定锁的初始状态,问必胜状态。

思路:
显然锁的状态是一张二分图,因为数位和奇偶性相同的状态显然无法一次到达。那么我们只要把非法的状态不加入图中,然后采用第一种判断方法就可以了。

code

#include<bits/stdc++.h>
using namespace std;
#define ll int
const ll INF=1e8;
const ll inf=1e8;
const int maxn=1e6+10,maxm=9e6+10;
const ll N=1e6+10;


struct MaximumFlow
{

    int h[maxn],e[maxm],ne[maxm],f[maxm],idx;
    int d[N],cur[N];
    int n,m,S,T,mxn;
    void init(int n,int _s,int _t)
    {
        mxn=n,idx=0,S=_s,T=_t;
        // memset(h,-1,sizeof h);
        while(n>=0)h[n--]=-1;
    }
    void addedge(int a,int b,int c)
    {
        ne[idx]=h[a],e[idx]=b,f[idx]=c,h[a]=idx++;
        ne[idx]=h[b],e[idx]=a,f[idx]=0,h[b]=idx++;
    }
    bool bfs()
    {
        memset(d,-1,sizeof d);
        d[S]=0;
        queue<int>q;
        q.push(S);
        cur[S]=h[S];
        while(q.size())
        {
            int t=q.front();
            q.pop();
            for(int i=h[t];i!=-1;i=ne[i])
            {
                int j=e[i];
                if(d[j]==-1&&f[i])
                {
                    d[j]=d[t]+1;
                    cur[j]=h[j];
                    if(j==T)return true;
                    q.push(j);
                }
            }
        }
        return false;
    }
    int find(int u,int limit)
    {
        if(u==T)return limit;
        int flow=0;
        for(int i=cur[u];i!=-1&&flow<limit;i=ne[i])
        {
            cur[u]=i;
            int j=e[i];
            if(d[j]==d[u]+1&&f[i])
            {
                int t=find(j,min(f[i],limit-flow));
                if(!t)d[j]=-1;
                f[i]-=t,f[i^1]+=t,flow+=t;
            }
        }
        return flow;
    }
    int dinic()
    {
        int r=0,flow;
        while(bfs())while(flow=find(S,inf))r+=flow;
        return r;
    }
}f;
ll n,m,pwd;
ll vis[N];
ll c[]={1,10,100,1000,10000,100000};
ll gt(ll x)
{
    ll sum=0;
    while(x)
    {
        sum+=x%10;
        x/=10;
    }
    return sum%2;
}
void solve()
{
    cin>>m>>n>>pwd;
    memset(vis,0,sizeof vis);
    for(int i=1;i<=n;++i)
    {
        ll a;cin>>a;vis[a]=1;
    }
    ll up=1;for(int i=1;i<=m;++i) up*=10;
    ll S=up,T=up+1;
    f.init(up+5,S,T);
    for(int i=0;i<up;++i)
    {
        //判断pwd是否一定在二分图的最大匹配中,一开始不将其与S或T连边
        //但是与网络其他节点相连。跑完网络流之后将pwd与对应起点/终点相连。
        //利用残量网络再跑一遍网络流。如果值大于0,代表其一定在二分图的最大匹配中
        if(vis[i]) continue;
        if(gt(i))
        {
            if(i!=pwd) f.addedge(i,T,1);
            continue;
        }
        if(i!=pwd) f.addedge(S,i,1);

        ll x;
        for(int j=0;j<m;++j)
        {
            x=i;
            int num=i;
            x/=c[j];
            x%=10;
            num=num-x*c[j];
            ll t1=num+((x+1)%10)*c[j];
            f.addedge(i,t1,1);
            t1=num+((x-1+10)%10)*c[j];
            f.addedge(i,t1,1);
        }
    }
    f.dinic();
    if(gt(pwd)) f.addedge(pwd,T,1);
    else f.addedge(S,pwd,1);
    if(f.dinic()) cout<<"Alice"<<endl;
    else cout<<"Bob"<<endl;
}
int main()
{
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    ll t;cin>>t;while(t--)
    solve();
    return 0;
}

[NOI2011] 兔兔与蛋蛋游戏

大意:

懒得写了,自己去读吧

思路:
有幸做出人生第二道黑题(敲下来有阻塞,但没想象中那么费劲)

我们稍微转化一下,将黑白棋的移动看成空格的移动。先手操作的时候,空格只能与白棋交换,此空格相当于黑棋。后手操作的时候,空格只能与黑棋交换,此时空格相当于白棋。所以相当于轮流在黑白棋之间切换,每次只能与不同颜色的点交换位置,显然是一张二分图。此外有一个性质比较隐蔽,那就是我们无法访问之前访问过的局面。因为黑白棋是轮流操作的,黑棋走过的点白棋是无法走的,反之同理。由此,我们成功转化成了二分图博弈,

这题要求我们输出所有操作失误的步骤。其实就是操作前后的都处于必胜状态的点。(仔细想想,不难理解)。每次操作之后整张图都不一样了,我们要判断的起始点也不一样了,所以我们每次要重新建图,因为图比较小,所以该方法是可行的。(但是还是要注意一下常数,本人吸了氧才过www)

code

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define endl '\n'
const ll N=110;
const ll INF=0x3f3f3f3f,inf=INF;
const ll maxn=4000,maxm=maxn*20;
struct MaximumFlow
{
    //maxn 最多点数 maxm 最多边数
    int h[maxn],e[maxm],ne[maxm],f[maxm],idx;
    int d[maxn],cur[maxn];
    int n,m,S,T,mxn;
    ll mp_vis[maxn];
    ll col[maxn],ans[maxn];
    ll flag=0;
    void init(int n,int _s,int _t)
    {
        mxn=n,idx=0,S=_s,T=_t;//mxn表示最大点的大小(包括S,T),用于初始化处理,_s表示源点,_T表示汇点
        // memset(h,-1,sizeof h);
        while(n>=0)
            h[n--]=-1;
    }
    void addedge(int a,int b,int c)
    {
        ne[idx]=h[a],e[idx]=b,f[idx]=c,h[a]=idx++;
        ne[idx]=h[b],e[idx]=a,f[idx]=0,h[b]=idx++;
    }
    bool bfs()
    {
        // memset(d,-1,sizeof d);
        for(int i=0;i<=mxn;++i) d[i]=-1;
        d[S]=0;
        queue<int>q;
        q.push(S);
        cur[S]=h[S];
        while(q.size())
        {
            int t=q.front();
            q.pop();
            for(int i=h[t];i!=-1;i=ne[i])
            {
                int j=e[i];
                if(d[j]==-1&&f[i])
                {
                    d[j]=d[t]+1;
                    cur[j]=h[j];
                    if(j==T)return true;
                    q.push(j);
                }
            }
        }
        return false;
    }
    int find(int u,int limit)
    {
        if(u==T)return limit;
        int flow=0;
        for(int i=cur[u];i!=-1&&flow<limit;i=ne[i])
        {
            cur[u]=i;
            int j=e[i];
            if(d[j]==d[u]+1&&f[i])
            {
                int t=find(j,min(f[i],limit-flow));
                if(!t)d[j]=-1;
                f[i]-=t,f[i^1]+=t,flow+=t;
            }
        }
        return flow;
    }
    int dinic()
    {
        int r=0,flow;
        while(bfs())while(flow=find(S,inf))r+=flow;
        return r;
    }
    bool judge(ll x,ll y)
    {
        for(int i=h[x];i!=-1;i=ne[i])
        {
            ll Y=e[i];
            if(Y==y) return 1;
        }
        return 0;
    }
}f;
ll n,m;
char mp[N][N];
ll px,py;
ll S,T;
vector<ll> ans;
ll gt(ll x,ll y)
{
    return y+(x-1)*m;
}
ll dir[][4]={{1,0},{-1,0},{0,1},{0,-1}};
bool inmap(ll x,ll y)
{
    return x>=1&&y>=1&&x<=n&&y<=m;
}
bool jd(ll x,ll y)
{
    if(x==px&&y==py) return 1;//是当前操作点
    return 0;
}
inline ll gt()
{
    //奇数点与汇点相连
    //偶数点与源点相连
    f.init(n*m+5,S,T);//每次要将起始点与源点/汇点断开连接,重新建图,反正图也不大
    for(int i=1;i<=n;++i)
    {
        for(int j=1;j<=m;++j)
        {
            if(jd(i,j)) continue;//去掉操作起始点
            if((i+j)%2==0) continue;
            f.addedge(gt(i,j),T,1);
        }
    }
    for(int i=1;i<=n;++i)
    {
        for(int j=1;j<=m;++j)
        {
            if((i+j)%2) continue;//去掉操作起始点
            if(!jd(i,j)) f.addedge(S,gt(i,j),1);
            for(int k=0;k<4;++k)
            {
                ll xx=i+dir[k][0];
                ll yy=j+dir[k][1];
                if(!inmap(xx,yy)) continue;
                if(mp[xx][yy]==mp[i][j]) continue;
                f.addedge(gt(i,j),gt(xx,yy),1);
            }
        }
    }
    f.dinic();
    if((px+py)%2) f.addedge(gt(px,py),T,1);
    else f.addedge(S,gt(px,py),1);

    return f.dinic();
}

void solve()
{
    cin>>n>>m;
    for(int i=1;i<=n;++i)
    {
        for(int j=1;j<=m;++j)
        {
            cin>>mp[i][j];
            if(mp[i][j]=='.')
            {
                px=i;py=j;
            }
        }
    }
    ////////////////////////


    S=n*m+1,T=n*m+2;
    ll op;cin>>op;
    for(int dx=1;dx<=op;++dx)
    {
        ll a,b;cin>>a>>b;
        mp[px][py]='X';//因为先手操作之后不能返回当前局面,所以要将这个位置置为X
        ll pre_1=gt();
        mp[px][py]='.';//
        swap(mp[px][py],mp[a][b]);
        px=a;py=b;

        mp[px][py]='O';//与上面改成X是同理的
        ll pre_2=gt();
        mp[px][py]='.';//
        // cout<<dx<<' '<<px<<' '<<py<<' '<<pre_1<<' '<<pre_2<<endl;
        if(pre_1&&pre_2)
        {
            ans.push_back(dx);
            //如果前后都是必胜态,那么该操作失误
        }
        ///
        cin>>a>>b;
        swap(mp[px][py],mp[a][b]);
        px=a;py=b;
    }

    cout<<ans.size()<<endl;
    for(auto i:ans) cout<<i<<endl;
}
int main()
{
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    solve();
    return 0;
}

留个小联系:[JSOI2009] 游戏

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值