bzoj2463 谁能赢呢?
题目大意:给定一个n×n的方格,从(1,1)开始走,每次可以到上下左右没有到过的一个格子,alice先手,交替操作,如果先手必胜则输出'Alice’,否则输出‘Bob’。
思路:lcomyn大爷秒暴结论。后来仔细想了想,只发现走一步肯定会改变格子奇偶性。其实如果n是偶数,那么就可以用2×1的骨牌覆盖,每次走到另一端后,另一个人走到新格子,所以先手必胜;如果n是奇数,那么就可以去掉第一个格子后骨牌覆盖,胜负正好反过来。
#include#include 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"); } }
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<
答案是总-先手必败=c(n,k)-fi[up][i]*c(n-i-k/2,k/2)(对于有i个石子的方案,从n-i个位置中选出k对两两相临)
因为题目中没有之前的假设,所以可能会导致从必败态->必败态,这种转化就是错误的。
#include#include #include #include #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 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 i) for (j=0;j<=n-k;++j){ if (!fi[i][j]) continue; for (q=0;j+(a=q*(d+1))*(1<>1);++q) add(fi[i+1][j+a*(1<>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); }
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#include #include #include #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"); } }
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#include #include #include #include #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 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(" "); } }
二分图博弈
bzoj2437 兔兔与蛋蛋
题目大意:给定一张棋盘,有白子黑子和空格子,兔兔先手,选一个白格子移到空格子;蛋蛋后手,选一个黑格子移到空格子。给出一个操作序列,问兔兔那几个操作是失误的(认为是失误的当且仅当兔兔操作前有必胜策略,兔兔操作后也有必胜策略)。
思路:黑白染色,相当于空格子走一些黑白交替的无环的路径,不能操作的人输。把空格子和黑格子看作一样,对那些本身颜色和黑白染色一样的格子编号(只有这些格子可能移动)。如果一个点一定在二分图最大匹配中,认为这个点有必胜策略。所以这道题目中要判断每次兔兔操作前后是否有必胜策略,如果都有,就认为它失误了。动态的二分图匹配。
#include#include #include #include #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); }
bzoj1443 游戏
题目大意:给定一个网格,有些格子不能走,有一枚棋子,每次走到相邻的格子。问选那些起始位置能使后手赢。
思路:黑白染色,如果一个点不一定在最大匹配上,就是后手赢。所以在匹配每个点是不是一定在就可以了。
#include#include #include #include #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"); }
还有一种写法比较快,用网络流求出最大匹配之后,如果从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#include #include #include #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 i) for (j=0;m3[j]*m2[i] 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 i){ m2[i]=m2[i-1]<<1; m3[i]=m3[i-1]*3; }for (i=1;i 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 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"); } }