博弈论

bzoj2463 谁能赢呢?

题目大意:给定一个n×n的方格,从(1,1)开始走,每次可以到上下左右没有到过的一个格子,alice先手,交替操作,如果先手必胜则输出'Alice’,否则输出‘Bob’。

思路:lcomyn大爷秒暴结论。后来仔细想了想,只发现走一步肯定会改变格子奇偶性。其实如果n是偶数,那么就可以用2×1的骨牌覆盖,每次走到另一端后,另一个人走到新格子,所以先手必胜;如果n是奇数,那么就可以去掉第一个格子后骨牌覆盖,胜负正好反过来。

#include<iostream>
#include<cstdio>
using namespace std;
int main()
{
    int n;
    while(scanf("%d",&n)==1)
    {
        if (n==0) break;
        if (n%2) printf("Bob\n");
        else printf("Alice\n");
    }
}
View Code

 

bzoj2281 黑白棋(!!!)

题目大意:一行1*n棋盘,交替放共k个白黑棋子,两个人交替操作,先手黑,后手白,可以选1~d个棋子移动,问先手必胜的初态有几种。

思路:假设白棋子左移和黑棋子右移没有意义,所以可以把一个白黑棋子看作一对,中间的棋子数是石子数。变成了每次可以从k/2堆石子中选1~d堆任取石子(每堆取得可以不同),先手必败态是:对二进制的每一位,这一位是1的石子堆数%(d+1)都是0(nimk游戏!!!,有一堆n个石子,每次可以取1~k任意个,先手必败态是:n%(k+1)=0,因为先手取x,后手可以取k+1-x)。设fi[i][j]表示前i-1个二进制位,选了j个石子,fi[i+1][j+q*(d+1)*(1<<i)]+=fi[i][j]*c(k/2,q*(d+1))。

答案是总-先手必败=c(n,k)-fi[up][i]*c(n-i-k/2,k/2)(对于有i个石子的方案,从n-i个位置中选出k对两两相临)

因为题目中没有之前的假设,所以可能会导致从必败态->必败态,这种转化就是错误的。

#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
#define N 10005
#define M 105
#define up 15
#define p 1000000007LL
#define LL long long
using namespace std;
LL fi[up+1][N],fac[N],inv[N];
LL mi(LL x,int y){
    LL a=1LL;
    for (;y;y>>=1){
        if (y&1) a=a*x%p;
        x=x*x%p;
    }return a;}
LL getc(int n,int m){
    if (n<m) return 0LL;
    return fac[n]*inv[m]%p*inv[n-m]%p;}
void add(LL &a,LL b){a=((a+b)%p+p)%p;}
int main(){
    int n,k,d,i,j,q,a;LL ans;scanf("%d%d%d",&n,&k,&d);
    for (fac[0]=1LL,i=1;i<=n;++i) fac[i]=fac[i-1]*(LL)i%p;
    for (inv[n]=mi(fac[n],(int)p-2),i=n-1;i>=0;--i) inv[i]=inv[i+1]*(LL)(i+1)%p;
    for (fi[0][0]=1LL,i=0;i<up;++i)
      for (j=0;j<=n-k;++j){
          if (!fi[i][j]) continue;
          for (q=0;j+(a=q*(d+1))*(1<<i)<=n-k&&a<=(k>>1);++q)
              add(fi[i+1][j+a*(1<<i)],fi[i][j]*getc(k>>1,a)%p);
      }
    for (ans=getc(n,k),i=0;i<=n-k;++i)
      add(ans,-(fi[up][i]*getc(n-i-(k>>1),k>>1)%p));
    printf("%I64d\n",ans);
}
View Code

 

sg函数

一个状态的sg值是它所有后继状态sg值得mex(最小未出现的自然数),sg=0是必败状态。大多情况下,多个子问题的sg的nim和!=0就是总问题可以必胜。

bzoj1228 E&D

题目大意:给定n堆石子,两堆一组,每次可以从一组中扔掉一堆,将另一堆分为两堆(每堆个数>=1),问先手有无必胜策略。

思路:sg函数在很多情况下可以通过打表找规律,然后求nim和判断。这道题目中的sg函数很有规律,斜着看是一个个的三角形,并且有递归的感觉,所以可以从2^30往下递归一下,相应的减去2^x就可以了,最后如果xy都是1,就是0。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define LL long long
using namespace std;
int sg(int x,int y){
    int i=1<<30,j=30,ans=31;
    for (;j;--j,i>>=1){
        if (x<=i&&y<=i) ans=j;
        else{
            if (x>i) x-=i;
            if (y>i) y-=i;
        }
    }if (x==1&&y==1) return 0;
    return ans;}
int main(){
    int t,i,j,n,u,v,cnt;
    scanf("%d",&t);
    while(t--){
        scanf("%d",&n);n/=2;cnt=0;
        for (i=1;i<=n;++i){
            scanf("%d%d",&u,&v);
            cnt^=sg(u,v);
        }if (cnt) printf("YES\n");
        else printf("NO\n");
    }
}
View Code

 

bzoj3576 江南乐

题目大意:给定n堆石子,两个人轮流操作,操作是:挑一堆不小于f个的,分成不小于2的任意堆,且要求尽量均分。不能操作的人输。

思路:考虑求sg[i],可以循环j从2~i表示分的堆数,k=i/j,所以i/j+1的有k1=i%j个,i/j的有k2=j-i%j个,这些sg值的异或就是答案(因为两个相同数的异或是0,所以只需要i%j^1=1才会^sg[i/j+1],sg[i/j]同理)。但因为i/j的取值只有根i个,且只需要考虑k1、k2的奇偶就可以了,所以可以做到n根n的复杂度。写成记忆化搜索会快很多。

#include<iostream> 
#include<cstdio> 
#include<cstring> 
#include<algorithm> 
#include<ctime> 
#define N 100001 
using namespace std; 
int sg[N]={0},vi[N]={0},f; 
void pre(){ 
    int i,j,k,k1,k2,la; 
    for (i=f;i<N;++i){ 
        for (j=2;j<=i;j=la+1){ 
            k=i/j;la=i/k; 
            k2=i%j;k1=j-k2; 
            vi[sg[k*(k1&1)]^sg[(k+1)*(k2&1)]]=i; 
            if (j<min(i,la)){ 
                ++j;k2=i%j;k1=j-k2; 
                vi[sg[k*(k1&1)]^sg[(k+1)*(k2&1)]]=i; 
            } 
        }for (j=0;vi[j]==i;++j); 
        sg[i]=j; 
    } 
} 
int main(){ 
    int t,i,j,n,ci; 
    scanf("%d%d",&t,&f);pre(); 
    while(t--){ 
        scanf("%d",&n);ci=0; 
        for (i=1;i<=n;++i){ 
            scanf("%d",&j);ci^=sg[j]; 
        }if (ci) printf("%d",1); 
        else printf("%d",0); 
        if (!t) printf("\n"); 
        else printf(" "); 
    } 
}
View Code

 

二分图博弈

bzoj2437 兔兔与蛋蛋

题目大意:给定一张棋盘,有白子黑子和空格子,兔兔先手,选一个白格子移到空格子;蛋蛋后手,选一个黑格子移到空格子。给出一个操作序列,问兔兔那几个操作是失误的(认为是失误的当且仅当兔兔操作前有必胜策略,兔兔操作后也有必胜策略)。

思路:黑白染色,相当于空格子走一些黑白交替的无环的路径,不能操作的人输。把空格子和黑格子看作一样,对那些本身颜色和黑白染色一样的格子编号(只有这些格子可能移动)。如果一个点一定在二分图最大匹配中,认为这个点有必胜策略。所以这道题目中要判断每次兔兔操作前后是否有必胜策略,如果都有,就认为它失误了。动态的二分图匹配。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define N 45
#define M 10000
using namespace std;
int mp[N][N],mt[M]={0},vi[M]={0},tot,point[M]={0},next[M],en[M],fb[M]={0},mi[M],
    id[N][N]={0},dx[4]={1,0,-1,0},dy[4]={0,1,0,-1};
int in(){
    char ch=getchar();
    while(ch!='.'&&ch!='O'&&ch!='X') ch=getchar();
    if (ch=='.') return -1;
    return ch=='X';}
int ab(int x){return (x<0 ? -x : x);}
void add(int u,int v){next[++tot]=point[u];point[u]=tot;en[tot]=v;}
bool find(int u){
    int i,v;
    for (i=point[u];i;i=next[i]){
        if (vi[v=en[i]]==tot||fb[v]) continue;
        vi[v]=tot;
        if (!mt[v]||find(mt[v])){
            mt[v]=u;mt[u]=v;return true;
        }
    }return false;
}
int main(){
    int n,m,i,j,k,q,x,y,bx,by,nm=0;scanf("%d%d",&n,&m);
    for (i=1;i<=n;++i)
      for (j=1;j<=m;++j)
          if ((mp[i][j]=in())<0) mp[bx=i][by=j]=1;
    for (i=1;i<=n;++i)
      for (j=1;j<=m;++j)
        if ((mp[i][j]&&(ab(i-bx)+ab(j-by))%2==0)||(!mp[i][j]&&(ab(i-bx)+ab(j-by))%2==1))
          id[i][j]=++nm;
    for (tot=0,i=1;i<=n;++i)
      for (j=1;j<=m;++j)
        if (id[i][j])
          for (k=0;k<4;++k)
            if (id[x=i+dx[k]][y=j+dy[k]])
              add(id[i][j],id[x][y]);
    for (tot=0,i=1;i<=nm;++i)
        if (!mt[i]){++tot;find(i);}
    scanf("%d",&q);q<<=1;
    for (i=1;i<=q;++i){
        if (mt[id[bx][by]]){
            j=mt[id[bx][by]];
            mt[j]=mt[id[bx][by]]=0;
            fb[id[bx][by]]=1;
            ++tot;if(find(j)) mi[i]=1;
        }else{fb[id[bx][by]]=1;mi[i]=1;}
        scanf("%d%d",&bx,&by);
    }for (nm=0,i=1;i<=q;i+=2)
        if (!mi[i]&&!mi[i+1]) ++nm;
    printf("%d\n",nm);
    for (i=1;i<=q;i+=2)
      if (!mi[i]&&!mi[i+1]) printf("%d\n",(i+1)/2);
}
View Code

 

bzoj1443 游戏

题目大意:给定一个网格,有些格子不能走,有一枚棋子,每次走到相邻的格子。问选那些起始位置能使后手赢。

思路:黑白染色,如果一个点不一定在最大匹配上,就是后手赢。所以在匹配每个点是不是一定在就可以了。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define N 105
#define M 40005
using namespace std;
int id[N][N],tot=0,point[M]={0},next[M],en[M],mt[2][M]={0},vi[M]={0},fb[M]={0},mp[N][N],tt=0,
    dx[4]={0,1,0,-1},dy[4]={1,0,-1,0};
int in(){
    char ch=getchar();
    while(ch!='.'&&ch!='#') ch=getchar();
    return ch=='.';}
void add(int u,int v){next[++tot]=point[u];point[u]=tot;en[tot]=v;}
bool find(int k,int u){
    int i,v;
    for (i=point[u];i;i=next[i]){
        if (vi[v=en[i]]==tt||fb[v]) continue;
        vi[v]=tt;
        if (!mt[k][v]||find(k,mt[k][v])){
            mt[k][v]=u;mt[k][u]=v;return true;
        }
    }return false;}
int main(){
    int n,m,i,j,k,x,y,cnt=0,nm=0;scanf("%d%d",&n,&m);
    for (i=1;i<=n;++i)
      for (j=1;j<=m;++j) if ((mp[i][j]=in())>0) id[i][j]=++nm;
    for (tot=0,i=1;i<=n;++i)
      for (j=1;j<=m;++j)
        if (id[i][j])
          for (k=0;k<4;++k)
            if (id[x=i+dx[k]][y=j+dy[k]])
              add(id[i][j],id[x][y]);
    for (i=1;i<=nm;++i)
      if (!mt[0][i]){++tt;find(0,i);}
    for (i=1;i<=n;++i)
      for (j=1;j<=m;++j){
          if (!id[i][j]) continue;
          if (!mt[0][id[i][j]]){
              if (!cnt) printf("WIN\n");
              printf("%d %d\n",i,j);
              ++cnt;continue;
          }else{
              for (k=1;k<=nm;++k) mt[1][k]=mt[0][k];
              fb[id[i][j]]=1;k=mt[1][id[i][j]];
              ++tt;mt[1][k]=mt[1][id[i][j]]=0;
              if (find(1,k)){
                if (!cnt) printf("WIN\n");
                  printf("%d %d\n",i,j);++cnt;
              }fb[id[i][j]]=0;
          }
      }if (!cnt) printf("LOSE\n");
}
View Code

还有一种写法比较快,用网络流求出最大匹配之后,如果从S通过有流量的边到的左边的点和从T通过没流量的边到的右边的点都是不一定出现的点,就是答案。

 

bzoj4600 硬币游戏(!!!

题目大意:给出一行n个硬币和mq,每次可以选一个反面向上的硬币c*2^i*3^j,(1)选定p、q,p>=1,q>=1&&p*q<=i&&q<=mq,可以把c*2^(i-k*q)*3^j翻过来;(2)选定p、q,p>=1,q>=1&&p*q<=j&&q<=mq,可以把c*2^i*3^(j-k*q)翻过来。

思路:可以看出和c没关系,可以对c的不同看作不同的游戏,sg异或起来就可以了。除去c之后,可以看作i*j的矩阵,每次可以翻i行或者j列的等差数列的棋子,可以把n个棋子看作x个只有右下角(i,j)是1的独立游戏的sg异或,因为每个棋子如果被翻偶数次是不影响先后手胜负的,对于每一个独立游戏,除了右下角的棋子翻了奇数次,其他都翻了偶数次,所以看作独立的是对的。枚举i、j、p、q看作是i、j的一种后继状态,后继又可以看作一个游戏,sg异或起来,求出mex就是ij的sg了。

(orz现场a掉的vampire爷)

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define N 15
#define M 21
#define up 30001
using namespace std;
int in(){
    char ch=getchar();int x=0;
    while(ch<'0'||ch>'9') ch=getchar();
    while(ch>='0'&&ch<='9'){
        x=x*10+ch-'0';ch=getchar();
    }return x;}
int sg[M][N][N],m2[N],m3[N],ai[up][2],vi[500]={0},vt=0;
void pre(int mq){
    int i,j,k,p,q,ci,mx;
    for (i=0;i<N;++i)
        for (j=0;m3[j]*m2[i]<up;++j){
            mx=max(i,j);++vt;
            for (q=1;q<=mq;++q)
                for (p=1;p*q<=mx;++p){
                    if (p*q<=i){
                        ci=-1;
                        for (k=1;k<=q;++k){
                            if (ci==-1) ci=sg[mq][i-p*k][j];
                            else ci^=sg[mq][i-p*k][j];
                        }if (ci!=-1) vi[ci]=vt;
                    }if (p*q<=j){
                        ci=-1;
                        for (k=1;k<=q;++k){
                            if (ci==-1) ci=sg[mq][i][j-p*k];
                            else ci^=sg[mq][i][j-p*k];
                        }if (ci!=-1) vi[ci]=vt;
                    }
                }
            for (k=0;vi[k]==vt;++k);
            sg[mq][i][j]=k;
        }
}
int main(){
    freopen("coin.in","r",stdin);
    freopen("coin.out","w",stdout);
    
    int n,m,i,j,t,ci;t=in();
    for (m2[0]=m3[0]=i=1;i<N;++i){
        m2[i]=m2[i-1]<<1;
        m3[i]=m3[i-1]*3;
    }for (i=1;i<up;++i){
        ai[i][0]=ai[i][1]=0;
        for (j=i;j%2==0;j>>=1) ++ai[i][0];
        for (j=i;j%3==0;j/=3) ++ai[i][1];
    }for (i=1;i<M;++i) pre(i);
    while(t--){
        n=in();m=in();ci=0;
        for (i=1;i<=n;++i)
            if (!in()) ci^=sg[m][ai[i][0]][ai[i][1]];
        if (ci) printf("win\n");
        else printf("lose\n");
    }
}
View Code

 

转载于:https://www.cnblogs.com/Rivendell/p/4765414.html

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值