[算法] 2-4 组合游戏
1、Nim游戏:有3堆火柴,分别为a,b,c根,记为状态(a,b,c)。每次一个游戏者可以从任意一堆中拿走至少一根火柴,也可以整堆拿走,但不能从多堆火柴中同时拿走。无法拿火柴的游戏者输。
2、组合游戏:Nim游戏其实就是组合游戏的一种,它满足:
a\两个游戏者轮流操作
b\游戏状态有限。并且不管双方怎么走,都不会出现以前出现过的状态。
c\谁不能操作谁输,另一个获胜。
3、状态图:为了方便分析,我们可以把游戏中的状态画成图。每个节点是一个状态,每条边代表从一个状态转移到另一个状态的操作(我们只讨论公平游戏,即:一个游戏者可以把状态A变为状态B,另一个游戏者也可以。)
4、两条规则:
a\一个状态必败当且仅当它所有的后继都是必胜状态。
b\一个状态必胜当且仅当它的后继有一个是必败状态。
*\特别的:没有后继的状态是必败状态。
有了这两条规则,就可以用递推的方法判断整个图的每一个结点是必胜态还是必败态。(注意到状态图都是无环的,所以如果按照拓扑排序的逆序进行判断,在判断每个节点的时候,他的所有后继都已经判断过啦。)
5、举个例子:
a\Ferguson游戏:有2个盒子,一开始其中一个有m颗糖而另一个有n颗糖,即状态(m,n)。每次操作是将一个盒子清空而把另一个盒子的一些糖拿到被清空的盒子中,使得2个盒子至少各有1颗糖。显然唯一的终止态为(1,1)。如果最后移动的游戏者获胜,那么状态为(m,n)的先手胜还是败?||-><-||根据第四点的规律我们按照k=m+n从小到大的顺序判断即可。下面代码输出所有k<20的必败态,由于(m,n)和(n,m)等价,这里只输出了n<=m的必败态。
1 #include<iostream> 2 using namespace std; 3 4 const int maxn = 100; 5 int winning[maxn][maxn]; 6 int main(){ 7 winning[1][1]=false; 8 for(int k=3;k<20;k++){//k由小到大枚举 9 for(int n=1;n<k;n++){//对于每个k枚举n 10 int m=k-n;//计算出状态k时的对应m 11 winning[n][m]=false;//先默认为输 12 for(int i=1;i<n;i++)//分析winning[n][m]后继之m倒掉 13 if(!winning[i][n-i]){winning[n][m]=true;break;} 14 for(int i=1;i<m;i++)//分析winning[n][m]后继之n倒掉 15 if(!winning[i][m-i]){winning[n][m]=true;break;} 16 if(n<=m && !winning[n][m])cout<<n<<' '<<m<<'\n'; 17 } 18 }return 0; 19 }
6、Nim sum:如果用上面相同的方法逆向判断所有节点,当a,b,c很大时,计算复杂度将会很高。其实早在1902年L.Bouton就提出了这样一个定理:状态(a,b,c)为必败态当且仅当a^b^c=0,这里的操作是二进制逐位异或操作(相同为0,不同为1),也成为Nim和(Nim sum)。
7、组合游戏的和:
a\定义:假设有k个组合游戏G1,G2,……,Gk,可以定义一个新游戏,在每个回合中,当前游戏者可以任选一个子游戏Gi进行一次合法操作,让其他游戏的局面保持不变,不能操作的游戏者输。这个新游戏称为G1,G2,……,Gk的和。
b\Nim:这样Nim游戏其实就是3个组合游戏G(从一堆火柴中拿走至少一根也可整堆拿走,无法拿的输)的和。为什么呢?因为每个回合里,当前游戏者可以选择一个子游戏(即选一堆火柴),然后进行一次合法操作(从该堆中拿火柴),但其他子游戏的局面保持不变(不能从多堆火柴中同时拿)。
c\SG定理和SG函数:组合游戏的和通常是很复杂滴,但是可以利用SG函数和SG定理来解决问题。||-><-||对于任意状态x,定义SG(x)=mes(S),其中S={SG(y)|y是x的后继状态},mex(S)表示不在S内的最小自然数。这样终态的SG值显然为0(S为空),而其他值可以递推出来,不难发现SG(x)=0当且仅当x为必败态。
8、类似Nim的游戏:
a\翻棋子游戏:一个棋盘上每个格子有一个棋子,每次操作可以随便选一个朝上的棋子(x,y)(代表x行y列),选一个形如(x,b)或者(a,y)(其中b<y,a<x)的棋子,然后把它和(x,y)一起旋转,无法操作的人输。||-><-||把坐标为(x,y)的棋子看做是大小分别为x,y的2堆火柴,每次操作是把其中一堆的数量减少或删除。
b\除法游戏:有一个n x m (1<=n,m<=50)矩阵,每个元素均为2-10000之间的正整数。两个游戏者轮流操作。每次可以选中一行中的1个或者多个大于1的整数,把他们中的每个数都变为它的某个素因子,比如:12可以变为1,2,3,4,6,不能操作的输(即:在操作之前矩阵全是1则输)。||-><-||考虑每个数的素因子个数,则让一个数“变为它的真因子”等价于拿掉它的一个或多个素因子。这样,每行对应一个火柴堆,每个数的每个素因子看成一根火柴,则本题和Nim完全等价啦。