Nim博弈
简述
通常的Nim游戏的定义是这样的:有若干堆石子,每堆石子的数量都是有限的,合法的移动是“选择一堆石子并拿走若干颗(不能不拿)”,如果轮到某个人时所有的石子堆都已经被拿空了,则判负(因为他此刻没有任何合法的移动)。
分析
这游戏看上去有点复杂,先从简单情况开始研究吧。如果轮到你的时候,只剩下一堆石子,那么此时的必胜策略肯定是把这堆石子全部拿完一颗也不给对手剩,然后对手就输了。如果剩下两堆不相等的石子,必胜策略是通过取多的一堆的石子将两堆石子变得相等,以后如果对手在某一堆里拿若干颗,你就可以在另一堆中拿同样多的颗数,直至胜利。如果你面对的是两堆相等的石子,那么此时你是没有任何必胜策略的,反而对手可以遵循上面的策略保证必胜。如果是三堆石子……好像已经很难分析了,看来我们必须要借助一些其它好用的(最好是程式化的)分析方法了,或者说,我们最好能够设计出一种在有必胜策略时就能找到必胜策略的算法。
定义和,其中代表,代表。直观的说,上一次移动石子的人有必胜策略的局面是,也就是“后手可保证必胜”或者“先手必败”,现在轮到移动石子的人有必胜策略的局面是,也就是“先手可保证必胜”。更严谨的定义是:
- 无法进行任何移动的局面(也就是)是;
- 可以移动到的局面是;
- 所有移动都导致的局面是。
按照这个定义,如果局面不可能重现,或者说的集合可以进行拓扑排序,那么每个或者是或者是,而且可以通过定义计算出来。
以游戏为例来进行一下计算。比如说我刚才说当只有两堆石子且两堆石子数量相等时后手有必胜策略,也就是这是一个,下面我们依靠定义证明一下是一个。首先的子局面(也就是通过合法移动可以导致的局面)有(显然交换石子堆的位置不影响其性质,所以把和看成同一种局面),只需要计算出这三种局面的性质就可以了。的子局面有,其中显然是,所以是(只要找到一个是的子局面就能说明是)。的后继中是(因为的唯一子局面是),所以也是。同样可以证明是。所以的所有子局面都是,它就是。通过一点简单的数学归纳,可以严格的证明“有两堆石子时的局面是当且仅当这两堆石子的数目相等”。
根据上面这个过程,可以得到一个递归的算法——对于当前的局面,递归计算它的所有子局面的性质,如果存在某个子局面是,那么向这个子局面的移动就是必胜策略。当然,可能你已经敏锐地看出有大量的重叠子问题,所以可以用或者记忆化搜索的方法以提高效率。但问题是,利用这个算法,对于某个游戏的局面来说,要想判断它的性质以及找出必胜策略,需要计算个局面的性质,不管怎样记忆化都无法降低这个时间复杂度。所以我们需要更高效的判断游戏的局面的性质的方法。
必胜态必败态
对于一个游戏的局面,它是当且仅当^^...^,其中^表示异或(xor)运算。
例题
例题1.hdu 1849
题意:游戏的规则是这样的:
- 棋盘包含1*n个方格,方格从左到右分别编号为0,1,2,…,n-1;
- m个棋子放在棋盘的方格上,方格可以为空,也可以放多于一个的棋子;
- 双方轮流走棋;
- 每一步可以选择任意一个棋子向左移动到任意的位置(可以多个棋子位于同一个方格),当然,任何棋子不能超出棋盘边界;
如果所有的棋子都位于最左边(即编号为0的位置),则游戏结束,并且规定最后走棋的一方为胜者。
给出初始棋子状态,输出先手赢,或者后手赢。
要求:Time Limit: 1000 MS , Memory Limit: 32768 K
思路:
#include<cstdio>
#include<cstring>
int num[1500];
int sg[1500];
int main()
{
int n;
while(~scanf("%d",&n))
{
if(n==0)
return 0;
int sum=0;
memset(num,0,sizeof(num));
for(int i=1;i<=n;i++)
{
scanf("%d",&num[i]);
sum^=num[i];
}
if(sum)
printf("Rabbit Win!\n");
else
printf("Grass Win!\n");
}
return 0;
}
扩充
相信大家觉得一道Nim博弈的例题根本不够做,不用担心,都在下一节(SG函数)呢,大家继续往下看吧~