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 }