游戏A 简单博弈
有两个游戏者:A和B。有21颗石子。两人轮流取走石子,每次可取1、2或3颗。A先取。取走最后一颗石子的人获胜,即没有石子可取的人算输。
如果剩下1、2或3颗石子,那么接下来取的人就能获胜;如果剩下4颗,那么无论接下来的人怎么取,都会出现前面这种情况,所以接下来取的人一定会输;如果剩下5、6或7颗石子,那么接下来取的人只要使得剩下4颗石子,他就能获胜。0,4,8,12,……都是下一个取石子者的必败状态。现在有21颗石子,21除以4的余数是1,所以先走者有必胜的策略,他第一次只要取走1颗石子,以后每一次都保证剩下的石子是4的倍数就行了。
P状态和N状态
就像第一个游戏一样,状态0,4,8,……是刚才走步的人的必胜状态,我们称之为P状态;
而1,2,3,5,6,7,……都是下一个走步的人的必胜状态,我们称之为N状态。
我们可以从终止状态出发,推出每一个状态,指出它是P状态还是N状态。
现在对游戏1略微扩展一下。
有一个决策集S,S中的元素是正整数。游戏的规则大致与游戏1一样,只是现在每次可以取的石子数必须是S中的元素。
如果S={1,2,3},那么就是游戏1。
大家分析一下,当S={1,3,4}的时候,哪些状态是P状态,哪些是N状态。
我们发现P状态是{0,2,7,9,14,16,……},N状态是{1,3,4,5,6,8,10,……}。
规律是如果n除以7的余数是0或2,那么状态n就是P状态,否则就是N状态。
如果游戏开始时,石子总数是100,那么这是一个P状态,也就是说后走的人有必胜策略。
游戏B Nim游戏
有三堆石子,分别含有x1,x2和x3颗石子。两人轮流取石子,每次可以选择一堆,从这堆里取走任意多颗石子,但不能不取。取走最后一颗石子的人获胜。
我们用三元组来表示状态,很明显(0, 0, 0)是唯一的终止状态,是P状态。
先考虑只剩一堆有石子的情况(0, 0, x),很明显这是,这些状态都是N状态。
剩两堆的情况,如果两堆的石子数相等(0, x, x),那么这些都是P状态。因为下一次走步的人一定会使得两堆石子不相等,再下一次可以使得两堆的石子数回到相等的状态,包括终止状态。如果两堆的石子数不相等,那么就是N状态。
“Nim和”就是两个数二进制表示的不进位加法,也就是两个整数进行xor位运算。
定义:两个数(xm…x0)2和(ym…y0)2,是(zm…z0)2,其中zi=(xi+yi) mod 2,0<=i<=m
整数关于Nim和(以后用“+”表示)满足交换律和结合律。
定理1:Nim游戏的一个状态(x1, x2, x3) 是P状态,当且仅当x1+x2+x3=0。
游戏C 图游戏
一个两人游戏,在一个图G(X, F)上玩,指明一个顶点x0并按照下列的规则:
A先走,从x0开始;
两人轮流走步;
从顶点x出发,只能走到顶点y,y属于F(x);
遇到终止状态,即不能走步,此人输。
后继函数定义:
用(X, F)来表示有向图G。X是顶点集,F是后继函数。设x是一个顶点,F(x)是一个集合,包含于X,任意一个元素y属于F(x),表示从x出发到y有一条边。F(x)就是x的后继集合,也可看成从x出发的决策集。如果F(x)是空集,那么就表示x是终止状态。
Sprague-Grundy函数定义:
对于一个递增有界的图G(X, F)来说,SG函数g,是定义在X上的函数,函数值是非负整数,使得
g(x)=min{n≥0|任意y∈F(x),n≠g(y)}
即g(x)的值等于所有x的后继的SG函数中没有出现的最小非负整数。
SG函数与P状态和N状态是有关的。如果g(x)=0,那么x就是P状态,否则x就是N状态。
如果x是终止状态,那么g(x)=0。
一个状态x,如果g(x)≠0,那么一定存在一个x的后继y,使得g(y)=0。
一个状态x,如果g(x)=0,那么所有x的后继y,都有g(y)≠0。
对于游戏B和游戏C本质上其实一致。有如下理论:
加法理论:
局面:(a1,a2,……,an)+(b1,b2,……,bm)=(a1,a2,……,an,b1,b2,……,bm)
对于局面A,B,S,若S=A+B,则称局面S可以分解为“子局面”A和B。
推论:若初始局面S可以分成两个相同的“子局面”,则先行者负(S负)。
分解理论:
对于局面S=A+B,若先行者有必胜策略,则称“S胜”,否则称“S负”。
1)若A与B一胜一负,则S胜。
2)若A负B负,则S负。
3)若A胜B胜,则S时胜时负。
推论:
1)S=A+C+C,则S的情况和A相同。故可用其简化局面,如局面(3,3,1)可以简化为局面(1)。
2)空局面是负局面。
一般解法:
用#S表示局面S对应的而二进制数,则:
局面S=A+B => #S=#A+#B
即#S等于该堆石子数a的sg函数值g(a)。如可以用#3表示(3)。(这里g(x)=x)
则有(3,3)=(3)+(3) => #(3,3)=#(3)+#(3)=11B+11B=0 (无进位2进制加法即异或运算)。
结论:若#S=0,则S负,否则S胜。
推广游戏B为每次取有上限m,则只需更改g(x)=x%(m+1)。
游戏D n个图游戏的并
定义:有n个递增有界的图游戏G1(X1, F1),……,Gn(Xn, Fn)。把它们合并成一个新的游戏G(X, F),记为G=G1+G2+…+Gn。X是所有游戏顶点集的笛卡尔积,即X=X1*X2*…*Xn。也就是说,我们用n元组(x1, x2, …, xn)来表示G中的顶点x,其中xi属于Xi,对于所有的i。x的后继F(x)可以定义成:
定理2:设G=G1+G2+…+Gn,Gi的SG函数是gi,i=1, 2, …, n。那么G的SG函数g(x1, x2, …, xn)=g1(x1)+g2(x2)+…+gn(xn),加法表示Nim和,即不进位的二进制加法。
必胜策略:只需要用合并后的SG值与每一堆SG值分别异或,看得到的结果是否小于原来该堆的SG值,如果小则可以取该堆。
游戏E 取走-分割游戏
甲乙面对若干排石子,其中每一排石子的数目可以任意确定。如初始局面为(7,3,3)。
每一步必须从某一排中取走两枚石子;
这两枚石子必须紧挨着。
分析:每取一次相当于再分堆。
一般解法:
1)用一个n元祖(a1,a2,……,an),来描述游戏中的一个局面。
2)用符号#S,表示局面S对应的二进制数。
3)用符号$(x),表示局面(X)的下一步所有可能出现局面的集合。
4)定义集合g(x),设:$(x)={S1,S2,……,Sk},则g(x)={#S1,#s2,……,#Sk}。
5)令非负整数集为全集,集合G(x)表示集合g(x)的补集。
6)定义函数f(n):f(n)=min{G(n)},即f(n)等于集合G(n)中的最小数。
7)设局面S=(a1,a2,……,an),#S=f(a1)+f(a2)+……+f(an),采用无进位二进制加法即异或运算。
若#S≠0,则S胜,否则S负。
题目推荐:
ZOJ Problem Set - 3529 A Game Between Alice and Bob
http://acm.zju.edu.cn/onlinejudge/showProblem.do?problemId=4464
题意:
Alice和Bob两个人在玩游戏,最开始有n个正整数,Alice先操作,之后双方轮流操作,可以任选一个数将其变为一个该数的一个和自己不等的约数,谁将所有数都变为1,谁就获胜了。
分析:
sg函数 g(n)={n的质因子的个数}。
求质因子个数先用快速帅选素数法标出素数以及每个数的最大质因子。
那么再处理这个数的质因子个数就是除以最大质因子之后的那个数的质因子个数+1(DP);
然后利用博弈问题的合并,可知,如果每个数得到的SG值进行异或,非0则胜,为0则负。然后后半部分又是另一个经典问题,只需要用合并后的SG值与每一堆SG值分别异或,看得到的结果是否小于原来该堆的SG值,如果小则可以取该堆。
给出个人代码参考(个人感觉还是比较清晰的):
#include "stdio.h" #define MAX 5000005 long isPrime[MAX]; long input[MAX]; long sg[MAX]; /** * 初始化素数表 * 若i为素数isPrime[i]=i 否则isPrime[i]为其最大质因子 */ void initPrime(){ long i,j; isPrime[0]=isPrime[1]=sg[0]=sg[1]=0; for(i=2;i<MAX;i++){ isPrime[i]=i; sg[i]=-1; } for(i=2;i<MAX;i++){ if(isPrime[i]==i){ for(j=2;i*j<MAX;j++) isPrime[i*j]=i; } } } /** * dp求取SG函数值 * n的质因子个数等于除以最大质因子之后的那个数的质因子个数+1 */ long getSG(long n){ if(sg[n]!=-1)return sg[n]; else if(isPrime[n]==n)return sg[n]=1; else return sg[n]=getSG(n/isPrime[n])+1; } int main() { long n,i,SG,count=1; initPrime(); while(scanf("%ld",&n)!=EOF){ for(SG=i=0;i<n;i++){ scanf("%ld",&input[i]); SG=SG^getSG(input[i]); } for(i=0;i<n;i++){ if((SG^getSG(input[i]))<getSG(input[i]))break; } if(SG!=0)printf("Test #%ld: Alice %ld\n",count++,i+1); else printf("Test #%ld: Bob\n",count++); } return 0; }