[BZOJ 2437][NOI 2011]兔兔与蛋蛋(二分图匹配)

题目链接

http://www.lydsy.com/JudgeOnline/problem.php?id=2437

思路

很容易发现整个游戏过程就是两个人依次移动棋盘上的唯一的那个空格。对整个棋盘进行黑白染色,假设空格是黑色格子(如下图中的被蓝色框出的’X’格子),并把空格看作’X’棋子,可以发现空格的移动轨迹是由黑格子上的’X’、白格子上的’O’交错的,如下图
[BZOJ 2437][NOI 2011]兔兔与蛋蛋(二分图匹配)_第1张图片
我们定义黑白染色后,起点空格为黑格子,这样的棋盘里所有的有’X’棋子的黑格子、有’O’棋子的白格子均为合法格子,将相邻的合法格子之间连无向边。问题转换为,在一个无向图里,初始时棋子在一个点上(这个点实际上对应于原来的棋盘里的空格子),兔兔和蛋蛋依次操作,每个人每次操作是将棋子从原来所在的点移动到相邻的点,并在无向图中删去原来的点,谁最先无法移动就算输。每次两个人依次对棋子移动结束后,判断当前状态是否是先手必败。

可以发现这个无向图实际上是个二分图,两侧分别对应于有’X’棋子的黑格子,有’O’棋子的白格子。这个二分图的一个匹配的交错轨就对应了一个合法的空格的移动轨迹。对于上述的博弈问题,有一个结论:若从点 x 开始出发,先手必胜,当且仅当点 x 是这个二分图最大匹配必须用上的点。

那么如果某一轮中,兔兔走之前是先手必胜态,兔兔走完了轮到蛋蛋走之前还是先手必败态,就说明兔兔走这一步走错了,本来是自己必胜,结果变成了蛋蛋必胜。

证明:如果起点不一定在最大匹配中(包括不可能在),那么第一步走到的点,一定在不包括起点的所有最大匹配中,否则起点与第一步走到的点,可以构成匹配,与是最大匹配矛盾。

因此若当前的点是未盖点,那么下一次走的点必定是匹配点。

若上一次后手走的是未盖点,那么这一次先手是必胜态。随便画个二分图以说明问题,无论如何,都是先手必胜,后手无路可走

游戏情况1


游戏情况2

这样,我们可以对兔兔和蛋蛋的每次操作开始之前,对整个二分图跑最大匹配,看看当前的空格对应于二分图里的点是否是必须在二分图最大匹配里的,是否是个未盖点。移动空格的操作结束后,在二分图里删去移动前的空格。

但是这样做太慢了,实际上我们只需要每次将当前的点和匹配它的点之间的关系断开,标记当前的点已被删除,若匹配它的点能找到新的匹配,则显然能说明当前的点不是必须在最大匹配里的点。

代码

#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <algorithm>

#define MAXE 12000
#define MAXV 3000
#define MAXN 1000

using namespace std;

struct edge
{
    int u,v,next;
}edges[MAXE];

int head[MAXV],nCount=0;

void AddEdge(int U,int V)
{
    edges[++nCount].u=U;
    edges[nCount].v=V;
    edges[nCount].next=head[U];
    head[U]=nCount;
}

int pre[MAXV],vis[MAXV],tot=0;
bool deleted[MAXV]; //deleted[i]=true表示点i已经删除了
bool res[MAXV];

bool DFS(int u)
{
    for(int p=head[u];p!=-1;p=edges[p].next)
    {
        int v=edges[p].v;
        if(deleted[v]) continue;
        if(vis[v]==tot) continue;
        vis[v]=tot;
        if(pre[v]==-1||DFS(pre[v]))
        {
            pre[v]=u;
            pre[u]=v; //!!!!
            return true;
        }
    }
    return false;
}

int n,m,q;

char map[MAXN][MAXN];
int num[MAXN][MAXN],cnt=0;

int main()
{
    memset(head,-1,sizeof(head)); //!!!!!
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
        scanf("%s",map[i]+1);
    int blankx,blanky; //空格位置
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
            if(map[i][j]=='.')
            {
                blankx=i;
                blanky=j;
                break;
            }
    map[blankx][blanky]='X'; //!!!!!!
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
            if((map[i][j]=='O'&&(abs(i-blankx)+abs(j-blanky))%2==1)||(map[i][j]=='X'&&(abs(i-blankx)+abs(j-blanky))%2==0))
                num[i][j]=++cnt;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
            if(num[i][j])
            {
                if(num[i-1][j]) AddEdge(num[i][j],num[i-1][j]); //注:这个二分图的交错轨就是一条合法的空格移动路径
                if(num[i+1][j]) AddEdge(num[i][j],num[i+1][j]);
                if(num[i][j-1]) AddEdge(num[i][j],num[i][j-1]);
                if(num[i][j+1]) AddEdge(num[i][j],num[i][j+1]);
            }
    memset(pre,-1,sizeof(pre)); //!!!!!
    for(int i=1;i<=cnt;i++)
        if(pre[i]==-1)
        {
            tot++;
            DFS(i);
        }
    scanf("%d",&q);
    q<<=1;
    for(int i=1;i<=q;i++)
    {
        if(pre[num[blankx][blanky]]!=-1) //!!!!
        {
            int tmp=pre[num[blankx][blanky]]; //重新进行一次增广
            pre[num[blankx][blanky]]=pre[tmp]=-1; //!!!!!
            deleted[num[blankx][blanky]]=true;
            tot++;
            res[i]=DFS(tmp);
        }
        else
        {
            deleted[num[blankx][blanky]]=true;
            res[i]=true;
        }
        scanf("%d%d",&blankx,&blanky);
    }
    int ans=0;
    for(int i=1;i<=q;i+=2)
        if(!res[i]&&!res[i+1])
            ans++;
    printf("%d\n",ans);
    for(int i=1;i<=q;i+=2) //i为奇数,res[i]=第i回合兔兔操作前,res[i+1]=第i回合兔兔操作后,true为下一次操作是必胜态,false为下一次操作是必败态
        if(!res[i]&&!res[i+1])
            printf("%d\n",(i+1)>>1);
    return 0;
}

你可能感兴趣的:([BZOJ 2437][NOI 2011]兔兔与蛋蛋(二分图匹配))