浅谈Nim博弈
最近两天学的Nim博弈,一些心得拿出来给大家分享一下,也算是记录一下自己的学习心得方便今后复习。本人水平有限,如有错误或不妥的地方,还请各位大牛多多指教(^_^)
尼姆博弈模型一般是这样的:
有三堆各若干个物品,两个人轮流从某一堆取任意多的物品,规定每次至少,取一个,多者不限,最后取光者得胜。给你三堆石头中的石头数:a,b,c,怎么样判断先手胜还是后手胜。
分析:
1.证明:
首先我们先自己模拟两个简单的例子, 三堆石子的数目分别是(0,2,2),我们可以枚举发现无论怎么走,先手都会输,另外一种情况,三堆石子的数目分别是(1,2,3),依然是先手输,因为无论先手怎么走,后手都可以将局势变成(0,n,n)这种局势。
那么我们再将局势推广到(0,n,n)的情况,发现如果先手取两个不为0的堆中的任何一个堆中的任何数量的石子,后手都可以对另外一个堆采取相同的操作,使局势依旧保持在(0,n,n)的状态,一直保持这样,直到最后先手不得不将一个堆中的石子全部取玩,那么后手马上可以将另外一个堆中剩余的石子取光,所以还是先手必胜。
那么,也就是说我们现在知道有三个奇异局势,分别是(0,0,0),(1,2,3),(0,n,n),那这三个局势看起来似乎没有什么联系。
真的没有吗?当然不可能,经过计算之后我们会发现这对三个奇异局势(a,b,c)都有a ^ b ^ c = 0。
那么也就是说这可能是奇异局势的特征咯,到底是不是没关系,我们可以证明一下:
首先定义P-position和N-position,其中P代表Previous,N代表Next。简单的说,就是P是必败态,N是必胜态
更严谨的定义是:1.无法进行任何移动的局面(也就是terminal position)是P-position;2.可以移动到P-position的局面是N-position;3.所有移动都导致N-position的局面是P-position。
这里对以上几点定义做出一点解释:第一点,无法进行任何移动的局面当然是必败的;第二点,如果当前是必胜态,那么当然要保证自己走完这一步之后留给对手的是必败态,所以可以移动到P态的是N态;第三点,自己无论现在这一步怎么走,走完这一步之后,留给对手的都是必胜态,这种局势当然是必败态。
那么现在拿出我们上面的假设:任何奇异局势(a,b,c)都有a ^ b ^ c = 0。
要证明这个假设,我们需要证明三个命题
1.根据该假设,所有terminal position都是P-position。
证明:因为terminal position只有一个,就是都是0的情况,那么不管多少个0异或之后结果肯定还是0。
2.根据该假设,当前被判为N-position的局势一定存在某种合法的移动,使得局势转变为P-position。
证明:对于任意一个局势(a1,a2........an)如果a1 ^ a2 ^...... ^ an != 0,那么设k = a1 ^ a2 ^...... ^ an ,从a1到an中一定存在一个数ai,它的二进制表示在k的最高位上为1,不然的话k二进制最高位上的1怎么来的。那么我现在设 s = ai ^ k = a1 ^ a2 ^.......^ a(i-1) ^ a(i+1) ^ .......an,则此时s二进制表示的最高位一定为0。(因为s ^ ai的最高位为1)所以, ai ^ k < ai成立,所以我们设ai ' = ai ^ k,则此时a1^a2^...^ai ' ^...^an = ai ' ^ (ai ^ k) = (ai ^ k)^(ai ^ k)= 0。所以存在一个合法的移动使得N-position转变为P-position局势。
3.根据该假设,当前被判断为P-position的局面无法移动到某个P-position。
证明:假设将ai改变成ai '能使得a1^a2^...^ai^...^an = a1^a2^...^ai ' ^...^an = 0,那么ai = ai ',而这种移动是不合法的。所以,当前被判断为P-position的局面无法移动到某个P-position。
那么我们现在就证明了对任意奇异局势(a1,a2,....an)都有a1^a2^...^an = 0的特征,同时,也将尼姆博弈模型从3堆推广到n堆。其实,这就是Bouton定理。
Bouton定理:状态(x1,x2,x3)为必败状态当且仅当x1^x2^x3=0,称为Nim和。
好了,现在证明完了,我们应该考虑如果在Nim博弈中获胜了,换句话说,在双方都采取最优策略的情况下,如何在己方是必胜态的情况下进行操作。
假定我们现在为先手,且局势为必胜局势。也就是a1 ^ a2 ^ ..... ^ an != 0,那么现在我们要将其中一个数ai减小为ai ',使得a1^a2^...^ai ' ^...^an = 0。首先,我们设k =a1^a2^...^ai^...^an,s = ai ^ k,那么问题就转化成了要找到一个 ai ' ,使得s ^ ai ' = 0,即 ai ’ = ai ^ k,再从数量为ai 的那堆中取出数量为ai - (ai ^ k)的数量的石头就可以了。
接下来附上两道例题,恰好解决的就是这个问题
例题一:
Being a Good Boy in Spring Festival
Time Limit: 1000/1000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others)Total Submission(s): 3974 Accepted Submission(s): 2347
春节回家 你能做几天好孩子吗
寒假里尝试做做下面的事情吧
陪妈妈逛一次菜场
悄悄给爸爸买个小礼物
主动地 强烈地 要求洗一次碗
某一天早起 给爸妈用心地做回早餐
如果愿意 你还可以和爸妈说
咱们玩个小游戏吧 ACM课上学的呢~
下面是一个二人小游戏:桌子上有M堆扑克牌;每堆牌的数量分别为Ni(i=1…M);两人轮流进行;每走一步可以任意选择一堆并取走其中的任意张牌;桌子上的扑克全部取光,则游戏结束;最后一次取牌的人为胜者。
现在我们不想研究到底先手为胜还是为负,我只想问大家:
——“先手的人如果想赢,第一步有几种选择呢?”
3 5 7 9 0
1
#include<algorithm>
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
int main()
{
int n,i,sum,cnt;
int Nim[105];
while(scanf("%d",&n) && n)
{
cnt=0;
sum=0;
for(i=0;i<n;i++)
{
scanf("%d",&Nim[i]);
sum^= Nim[i];
}
for(i=0;i<n;i++)
{
if(Nim[i]>(sum^Nim[i]))//如上所述,判断是否存在ai > ai ^ k,如果存在,对该堆进行操作。
cnt++;
}
printf("%d\n",cnt);
}
return 0;
}
例题二:
取(m堆)石子游戏
Time Limit: 3000/1000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others)Total Submission(s): 1493 Accepted Submission(s): 869
2 45 45 3 3 6 9 5 5 7 8 9 10 0
No Yes 9 5 Yes 8 1 9 0 10 3
#include<algorithm>//和上一题几乎一样,只不过要输出具体的操作方案就是了。
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
int Nim[200050];
int main()
{
int cnt, n;
while(scanf("%d", &n) && n)
{
cnt = 0;
for(int i=0; i<n; ++i)
{
scanf("%d", &Nim[i]);
cnt ^= Nim[i];
}
if(cnt == 0)
{
printf("No\n");
continue;
}
printf("Yes\n");
for(int i=0; i<n; ++i)
{
if(Nim[i] >= (Nim[i] ^ cnt))
printf("%d %d\n", Nim[i], Nim[i] ^ cnt);
///对ai > ai ^ k的堆操作,使留下数量为ai ^ k,此时(ai ^ k) ^ (ai ^ k) = 0,使对手面对必败局势。
}
}
return 0;
}