文章PDF链接http://pan.baidu.com/share/link?shareid=138959164&uk=1913509805
写在前面的话
博弈一类的解题报告,许多都是给你个结论,包括博弈基础的三大经典模型,许多人也是知道结论,但是不知道为什么是这个结论,也就是知其然,不知其所以然。虽然一些结论,给出了证明过程,使我们知道了所以然,但是我们仍然只知道此一结论,题目可以千变万化,我们能把所有的结论知道吗?显然不能,所以我们需要知道结论的由来,大则可以是定理的发现过程,小则是我们解题的过程,思路。
我们每做一道题,就是一个探索的过程,由不会做到会做,由不知到知。这个过程很难用文字表达出来,或许很多人也懒得表达。一些人只顾做题,却忽略了总结,其实总结是知识归纳和经验积累的最好途径,当然也是创新的基础。
所以我提倡,解题报告不要只给解,应该详细的把思路呈现出来,让别人知道你是怎么想的,否则那终究是填鸭式教育,就像许多人做DP一样,会做的是会做,不会做的仍然是不会做,即使是你当时会做,那么过一段时间等记忆模糊了再做,你就不一定能解出答案。但如果你具备解题的这种能力,那么就算是来个新题,也不会手足无措,无从下手。
但是有时候思路是很难详细记录的,一个题的解出(尤其像博弈一类),思路(由不知到知)也是一个漫长的过程,举一反三是很难做到的。所以我们能做什么的,或许就是多做几道题,总结一下,大致分为几类,然后标记一些不容易想到的地方,为以后做此类题,少走弯路。
这次写的一些东西,也只是告诉大家我是怎么想的,但我是怎么“这样想”的。其中一些是,顺藤摸瓜,究果索因;还有一些是经验和直觉,根据现象找本质。当然有些东西妙不可言,是只能意会不能言传,所以我这里献出的仅仅是一些糟粕而已,真正的精华需要读者自行试之,悟之。
1847 Good Luck in CET-4 Everybody! 4
1850 Being a Good Boy in Spring Festival 8
1848 Fibonacci again and again 11
2147 kiki's game
简单的巴什博弈(bash game)详见《博弈基础知识总结》。
#include <stdio.h>
int main()
{
int n,m;
while(scanf("%d%d",&n,&m)!=EOF && (n || m))
{
puts((n*m)&1?"What a pity!":"Wonderful!");
}
return 0;
}
2188 悼念512汶川大地震遇难同胞——选拔志愿者
同样是巴什博弈这个经典模型,只不过原来是“取石子”,现在是“加石子”。
#include <stdio.h>
int main()
{
int n,m,c;
while(scanf("%d",&c)!=EOF)
{
while(c--)
{
scanf("%d%d",&m,&n);
puts((m%(n+1))?"Grass":"Rabbit");
}
}
return 0;
}
1846 Brave Game
还是巴什博弈,不解释。。。
#include <stdio.h>
int main()
{
int n,m,c;
scanf("%d",&c);
while(c--)
{
scanf("%d%d",&m,&n);
puts((m%(n+1))?"first":"second");
}
return 0;
}
1517 A Multiplication Game
题目给的范围比较大(1 < n < 4294967295),显然不能直接写SG函数,只能找到SG函数的规律。
该题按部就班的推导即可,可以发现,[1,9]为必胜,(9,18)为必败,(18,18*9]为必胜,(18*9,18*18]必败,依次类推……需要注意,这里需要用double,但是不能用int,否则结果可能因丢失精度而出错。
#include <stdio.h>
int main()
{
double n;
while(scanf("%lf",&n)!=EOF)
{
while(n>18)
n/=18;
puts(n>9?"Ollie wins.":"Stan wins.");
}
return 0;
}
1847 Good Luck in CET-4 Everybody!
由于SG的范围比较小,只有1000,而且每次规则不变,所以可以写一个初始化SG函数,然后非递归的从0开始正向推出所有SG值即可,可套模板。如果你懒得手推就可这样写,前提是SG函数比较规范化,容易写。做完后一打表发现规律很明显。
#include <stdio.h>
#include <string.h>
const int M=1010;
int sg[M],F[15];
bool b[M];
void Init()
{
int i,j,t;
sg[0]=0;
t=1;
for(i=0;;i++)
{
F[i]=t;
t*=2;
if(t>1000)
break;
}
t=i;
for(i=1;i<=M-10;i++)
{
memset(b,0,sizeof(b));
for(j=0;j<t && i>=F[j];j++)
b[sg[i-F[j]]]=1;
for(j=0;;j++)
{
if(!b[j])
{
sg[i]=j;
break;
}
}
}
}
int main()
{
int n;
Init();
while(scanf("%d",&n)!=EOF)
{
puts(sg[n]?"Kiki":"Cici");
}
return 0;
}
OR
#include <stdio.h>
int main()
{
int n;
while(scanf("%d",&n)!=EOF)
{
puts(n%3?"Kiki":"Cici");
}
return 0;
}
3863 No Gambling
这个题是各自操作自己的棋子,所以根据定义不算博弈,所以其结论皆不可用,这里可试着推导几个,你会发现先手必胜,这说明了两句俗语,那即是“先下手为强”,还有“棋输一步”。
#include <stdio.h>
int main()
{
int n;
while(scanf("%d",&n)!=EOF && n!=-1)
{
puts("I bet on Oregon Maple~");
}
return 0;
}
1536 S-Nim
典型的模板题,不解释,可以排下序用作剪枝,由于每次规则不同,再加上可能也用不到那么大数的SG值,所以只需一步一步向前递推求值即可,每次记录SG值可提高效率。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int a[100],k,sg[10001];
int cmp(const void *a,const void *b)
{
return (*(int *)a)-(*(int *)b);
}
int SG(int t)
{
bool b[100];
memset(b,0,sizeof(b));
int i,p;
for(i=0;i<k;i++)
{
p=t-a[i];
if(p<0)
break;
if(sg[p]==-1)
sg[p]=SG(p);
b[sg[p]]=1;
}
i=0;
while(b[i])i++;
return i;
}
int main()
{
int i,m,h,s,t;
while(scanf("%d",&k)!=EOF && k)
{
memset(sg,-1,sizeof(sg));
sg[0]=0;
for(i=0;i<k;i++)
scanf("%d",&a[i]);
qsort(a,k,sizeof(int),cmp);
scanf("%d",&m);
while(m--)
{
s=0;
scanf("%d",&h);
while(h--)
{
scanf("%d",&t);
if(sg[t]==-1)
sg[t]=SG(t);
s^=sg[t];
}
putchar(s?'W':'L');
}
puts("");
}
return 0;
}
1527 取石子游戏
威佐夫博奕(Wythoff Game),直接套结论,详细结论分析过程可看《博弈基础知识总结》,这里需要拐一点弯,如果每次从i=1开始遍历寻找必败点,会T的很惨。
#include <cstdio>
#include <cmath>
#include <iostream>
using namespace std;
int main()
{
int a,b,i;
while(scanf("%d%d",&a,&b)!=EOF)
{
if(a>b)
swap(a,b);
i=b-a;
if(a==(int)(i*(1+sqrt(5.0))/2))
puts("0");
else puts("1");
}
return 0;
}
1850 Being a Good Boy in Spring Festival
简单的Nim博弈,但这里需要给出必胜策略的走法,也就是怎么执行一步后可以使Nim-Sum为0,这里需要知道若a^b=c,则b^c=a;如果b变成b^c,那么,a^(b^c)=0,这是两堆的情况;多堆可自行证明。所以若b>=(b^c),那么这就是取这堆可使之败。
#include <stdio.h>
int main()
{
int M,i,s,t,a[101];
while(scanf("%d",&M)!=EOF && M)
{
t=s=0;
for(i=0;i<M;i++)
{
scanf("%d",&a[i]);
s^=a[i];
}
if(s)
{
for(i=0;i<M;i++)
{
if(a[i]>=(s^a[i]))
{
t++;
}
}
}
printf("%d\n",t);
}
return 0;
}
2149 Public Sale
简单的巴什博弈(bash game)详见《博弈基础知识总结》。
#include <stdio.h>
int main()
{
int i,n,m;
while(scanf("%d%d",&n,&m)!=EOF)
{
if(m>=n)
{
for(i=n;i<m;i++)
printf("%d ",i);
printf("%d\n",i);
}
else
{
if(n%(m+1))
printf("%d\n",n%(m+1));
else
puts("none");
}
}
return 0;
}
1730 Northcott Game
该题是Nim的变形,模型化亦可理解为有n堆石子(n为行数),石子数量即为坐标差的绝对值减1,唯一不同的是,这堆石子数貌似可以加,当然在有限的范围内。让我们看看这会不会影响结果,若当前是必败点即Sum-Nim=0;如果向反方向移动令石子数“增加”,对手依然可以取石子至原来的状态,依然是必败点,如果是必胜点,那直接下手就行了,不必后退了,对吧。
#include <stdio.h>
#include <math.h>
int main()
{
int n,a,b,F;
while(scanf("%d%*d",&n)!=EOF)
{
F=0;
while(n--)
{
scanf("%d%d",&a,&b);
F^=(abs(a-b)-1);
}
if(F)
puts("I WIN!");
else puts("BAD LUCK!");
}
return 0;
}
1079 Calendar Game
这个题看上去貌似很麻烦,但是你得老老实实的推导,会柳暗花明的,写一个月份日历表,然后表明N,P点即可解除,需注意的是两处特殊情况。
#include <stdio.h>
int main()
{
int T,m,d;
scanf("%d",&T);
while(T--)
{
scanf("%*d%d%d",&m,&d);
if(((m+d)&1)==0 ||(d==30 && (m==11||m==9)))
puts("YES");
else puts("NO");
}
return 0;
}
1848 Fibonacci again and again
典型的模板题,不过多解释。
#include <stdio.h>
#include <string.h>
const int M=1010;
int SG[M],F[M];
bool b[M];
void Init()
{
int i,j,Cnt;
F[1]=1;F[2]=2;
SG[0]=0;
for(i=3;;i++)
{
F[i]=F[i-1]+F[i-2];
if(F[i]>M)
break;
}
Cnt=i;
for(i=1;i<=M-10;i++)
{
memset(b,0,sizeof(b));
for(j=1;j<Cnt && i>=F[j];j++)
b[SG[i-F[j]]]=1;
for(j=0;;j++)
{
if(!b[j])
{
SG[i]=j;
break;
}
}
}
}
int main()
{
int m,n,p;
Init();
while(scanf("%d%d%d",&m,&n,&p)!=EOF && (m||n||p))
{
if((SG[m]^SG[n]^SG[p])!=0)
puts("Fibo");
else puts("Nacci");
}
return 0;
}
3951 Coin Game
SG函数不明显,或写着费劲,遂手推。显然在进行第一次动作之后,环就断了,如果最多取一个,可以判断奇偶来定结果,如果K大于1,则第二次操作一定可以把剩下的一堆(这里视作一堆)可以分为同样数目的2堆,第三次怎么操作,第四次只需在另一堆进行与上一次同样的操作即可,这样可以保持到最后,也就是Second必胜。当然如果K>=N,那么还是First必胜的。
#include <stdio.h>
int main()
{
int i,T,n,m;
scanf("%d",&T);
for(i=1;i<=T;i++)
{
scanf("%d%d",&n,&m);
printf("Case %d: ",i);
if(m>=n)
puts("first");
else
{
if(m==1)
puts(n&1?"first":"second");
else
puts("second");
}
}
return 0;
}
1564 Play a game
简单推导后发现的简单规律
#include <stdio.h>
int main()
{
int n;
while(scanf("%d",&n)!=EOF && n)
puts(n&1?"ailyanlu":"8600");
return 0;
}
2516 取石子游戏
根据题意推导了几个,发现好像是斐波那契数列,后来发现真是,但是不知道怎么证明。有待研究。。。
#include <stdio.h>
int F[44]={2,3};
int main()
{
int n,i;
for(i=2;i<44;i++)
{
F[i]=F[i-1]+F[i-2];
}
while(scanf("%d",&n)!=EOF && n)
{
for(i=0;i<44;i++)
if(F[i]==n)
break;
if(i==44)
puts("First win");
else puts("Second win");
}
return 0;
}
1849 Rabbit and Grass
依旧是Nim博弈模型
#include <stdio.h>
int main()
{
int n,t,i;
while(scanf("%d",&n)!=EOF && n)
{
t=0;
while(n--)
{
scanf("%d",&i);
t^=i;
}
puts(t?"Rabbit Win!":"Grass Win!");
}
return 0;
}
1729 Stone Game
当Ci为0,或者Si=Ci时显然对结果没有影响,这里对其他情况的SG值做一些讨论,设当前箱子里的石子数为x,那么一次执行后可以到达的x+x*x的石子数,也就是如果最多为x+x*x=Ci的两个根一正一负,设x1为正,则x1就是一个临界,(x1-1)也一定是必败点,显然可得SG(x1-2)=1;SG(x1-3)=2这样一直到下一个必败点,可以先找到与Si最从右接近的那个临界点,然后求的其SG值。
#include <stdio.h>
#include <math.h>
int main()
{
int N,i=0,t,c,C,s;
while(scanf("%d",&N)!=EOF && N)
{
t=0;
i++;
while(N--)
{
scanf("%d%d",&c,&s);
if(s==0 || c==s)
continue;
do
{
C=c;
c=(-1+sqrt(1.0+4*c))/2+0.999999;
c--;
}while(s<c);
if(s==c)
continue;
t^=C-s;
}
printf("Case %d:\n",i);
puts(t?"Yes":"No");
}
return 0;
}
1907 John
这个与Nim游戏唯一的不同是,胜负的判断变了,最后取完的为必败点了。这个与经典的Nim游戏恰恰相反,但是结论就相反么,不一定,我们看看那个万能的异或定理是不是满足这个游戏,如果我们记得Nim结论的证明的话,肯定会发现(详见《博弈论基础知识的一些总结》),需满足三个因素,只有第一条不满足,因为如果剩下一堆为1,这个是在这必败点,但经典的Nim中是必败点,其他两个因素依然满足,所以这个结论依旧差不多,特判一些情况就行了,SG(1)有变化,其他都没变。
#include <stdio.h>
int main()
{
int T,N,t,s,i,F;
scanf("%d",&T);
while(T--)
{
scanf("%d",&N);
s=F=0;
for(i=0;i<N;i++)
{
scanf("%d",&t);
if(t!=1)
F=1;
s^=t;
}
if(!F)
s^=1;
puts(s?"John":"Brother");
}
return 0;
}
1404 Digital Deletions
这道题,我们把每个数字看成一堆,分别求每个SG值有点困难,或者说不可行,因为,几堆之间是有联系的。我们这里采用的是爆搜,一共1000000个数,如果有前导零,则肯定必胜,可作为特例输出。否则分别求SG值,把SG数组初始化全为0,然后,假设a数字可以有b一步的来,如果SG(a)为0,则SG(b)=1;遍历所有点,然后求出SG值为1的,剩下的即是必败点。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
const int M=1000000;
bool sg[M]={1};
char a[10];
void Find(int x)
{
int l,i,j,m,n;
itoa(x,a,10);
l=strlen(a);
for(i=0;i<l;i++)
{
for(m=1,j=0;j<i;j++)
m*=10;
n=(x/m)%10;
for(n=9-n,j=1;j<=n;j++)
sg[x+j*m]=1;
}
if(l!=6)
{
for(n=1,i=l;i<6;i++)
{
x*=10;
for(j=0;j<n;j++)
sg[x+j]=1;
n*=10;
}
}
}
void Init()
{
int i;
for(i=0;i<M;i++)
if(!(sg[i]))
Find(i);
}
int main()
{
int i,s;
Init();
while(scanf("%s",a)!=EOF)
{
if(a[0]=='0')
{
puts("Yes");
continue;
}
s=0;
for(i=0;a[i]!=0;i++)
{
s*=10;
s+=a[i]-'0';
}
puts(sg[s]?"Yes":"No");
}
return 0;
}
1525 Euclid's Game
首先上来也是没有头绪的推导测试案例中(25,7),后来你会发现,(a,b)(不失一般性,我们假设a>=b)如果a>=2*b,那么这个点可以到达(a-b,b)还可以到达,(a-2b,b)对吧,这里我们假设(a-2b,b)必败,那么(a-b,b)和(a,b)必胜了,对吧;注意了,当(a-2b,b)必胜时,由于(a-b,b)只能到(a-2b,b),所以其必败,这样,(a,b)可以到达(a-b,b)所以其是不是也必胜了,对的。也就是当a>=2*b时,必胜,当然显而易见,a=b时也是必胜的。否则就只能递归求SG值了,注意,这里没有那么多堆,不用异或,所以直接返回0或1就行了。
#include <stdio.h>
#include <iostream>
using namespace std;
bool SG(int n,int m)
{
if(n>=2*m || n==m)
return 1;
else return !SG(m,n-m);
}
int main()
{
int n,m;
while(scanf("%d%d",&n,&m)!=EOF && (m||n))
{
if(n<m)
swap(n,m);
if(SG(n,m))
puts("Stan wins");
else puts("Ollie wins");
}
return 0;
}
2954 Marble Madness
两种颜色的球(B,W),三种取法,第一种是两个白的换一个黑的,后面两种都是去一个黑的,有两个黑的可以去,一个黑的和一个白的也可以去,显然,黑的可以一个一个去,白的只能一次取两个,所以,最后剩下黑的白的,取决于白色数的奇偶性。
#include <stdio.h>
int main()
{
int T,a,b;
scanf("%d",&T);
while(T--)
{
scanf("%d%d",&a,&b);
puts(b&1?"0.00 1.00":"1.00 0.00");
}
return 0;
}
2176 取(m堆)石子游戏
同1009 Being a Good Boy in Spring Festival
#include <stdio.h>
int a[200000];
int main()
{
int m,i,t;
while(scanf("%d",&m)!=EOF && m)
{
t=0;
for(i=0;i<m;i++)
{
scanf("%d",&a[i]);
t^=a[i];
}
if(!t)
{
puts("No");
continue;
}
else puts("Yes");
for(i=0;i<m;i++)
{
if(a[i]>=(t^a[i]))
{
printf("%d %d\n",a[i],t^a[i]);
}
}
}
return 0;
}
1524 A Chess Game
也算是模板题吧,只是题挺难读懂的。
#include <stdio.h>
#include <string.h>
const int M=1000;
bool a[M][M];
int sg[M],N,X;
int SG(int t)
{
bool b[100];
int i;
memset(b,0,sizeof(b));
for(i=0;i<N;i++)
{
if(!a[t][i])
continue;
if(sg[i]==-1)
sg[i]=SG(i);
b[sg[i]]=1;
}
i=0;
while(b[i])i++;
return i;
}
int main()
{
int i,t,s;
while(scanf("%d",&N)!=EOF)
{
memset(a,0,sizeof(a));
for(i=0;i<N;i++)
{
scanf("%d",&X);
while(X--)
{
scanf("%d",&t);
a[i][t]=1;
}
}
memset(sg,-1,sizeof(sg));
while(scanf("%d",&X)!=EOF && X)
{
t=0;
while(X--)
{
scanf("%d",&s);
if(sg[s]==-1)
sg[s]=SG(s);
t^=sg[s];
}
puts(t?"WIN":"LOSE");
}
}
return 0;
}
1760 A New Tetris Game
也算是一个模板题吧,写一个递归求SG函数,放置完后,递归后手所有可行的点得SG值,如果为0,则当前可返回1,否则为0。
#include <stdio.h>
#include <string.h>
char a[50][50];
int n,m;
bool SetJudge(int i,int j)
{
if(a[i][j]=='0' && a[i][j+1]=='0' && a[i+1][j]=='0' && a[i+1][j+1]=='0')
{
a[i][j]=a[i][j+1]=a[i+1][j]=a[i+1][j+1]='1';
return 1;
}
return 0;
}
void Unset(int i,int j)
{
a[i][j]=a[i][j+1]=a[i+1][j]=a[i+1][j+1]='0';
}
int SG()
{
int i,j;
for(i=0;i<n-1;i++)
for(j=0;j<m-1;j++)
{
if(SetJudge(i,j))
{
if(!SG())
{
Unset(i,j);
return 1;
}
Unset(i,j);
}
}
return 0;
}
int main()
{
int i;
while(scanf("%d%d",&n,&m)!=EOF)
{
for(i=0;i<n;i++)
scanf("%s",a[i]);
if(SG())
puts("Yes");
else puts("No");
}
}
总结
博弈可分为几种类型,其中一种为可以手工推出SG函数的,可以直接在代码上写公式,这种题目,给的范围一般比较大,直接给整型范围,肯定不能一点一点的求SG值,那样会超内存或者超时,所以这样题要么很简单,要么就是很难得,思路巨复杂;
还有的得写求SG的代码,SG函数又分为几种,有的是可以写一个初始化Init()分别求出各个SG值,每次用O(1)内就可以给结果,这个前提是,题目给的博弈规则是固定的,还有数据范围不能太大,否则空间和时间都容易超;
还有一种是就是SG函数需要求多次,例如每次取石子的数量是题目现给的,或者SG函数范围不算大,也不算小(一万或十万)时,因为如果太大,这样盲目的多次求SG函数肯定会超时,或者题目输入数据中可能根本用不到那么大数据的SG值。此时需要一个递归的SG函数,开一个数组记录以便提高效率例如HDU_S-Nim;
还有一些小技巧,如果题目就一堆“石子”,显然不用异或,此时只需只求SG是0或1就可以解题了;如果是多堆,一般得求出其每堆的SG(),然后异或,但是有例外,例如HDU_Digital Deletions,起初把他看成若干堆就不容易求,因为这些堆之间是联系的,这里不妨把堆放在一起,直接求出SG(a1,a2……)的值。
这个题给了我们一个提示,我们往常求其SG值时一般看根据执行一次后状态的SG值,如子节点没有0则为1,否则为0,这里给了我们提示,如果当前SG为0,那么凡是可以经过一步到达其的状态皆为必胜。显然这适用于,子节点比较复杂的那种,反其道而行之可能好些。这个思想我们以前就用过,例如求素数,
bool F(int n)
{
for(i=2;i*i<=n;i++)
if(n%i==0)
return 0;
return 1;
}
上面类比为传统求SG的方法,那下面这个就可以类比为刚才那个思想。
a[1001]={0,1};
for(i=2; i<=1000; i++)
if(a[i]==0)
for(j=i+i; j<=1000; j=j+i)
a[j]=1;
怎么样,其实有些东西是相通的,我们一旦收获一种思想,就要深入的理解,思考,这样以后用时才能想到,迪杰斯特拉就是对贪心的一次应用,发明人为什么是他,这肯定不是偶然。博弈用到递归其实就有些深搜的思想,所以有些题,难免与深搜结合;SG函数本来就是图论的东西过来的,所以博弈与图论的联系是很密切的,你做题时会发现,稿纸上的都是拓扑图,许多东西都是相通,我们一定要多思考。