以下内容许多都是看这里的:http://blog.csdn.net/acm_cxlove/article/details/7854530
尼姆博弈(Nim Game):两个人要玩一个游戏,有n堆纸牌,每堆有若干张,每人轮流取物纸牌,每次可以从其中一堆中取出若干张(不能不取),谁最后取完所有纸牌谁就获胜。
尼姆博弈有一个关键的定理:nim-sum是所有堆的纸牌数的异或值,比如有3堆纸牌,数量分别为a1,a2,a3。如果a1^a2^a3=0(这里的^是异或的意思),则先手必败,反之先手可以获胜。
结论一(取完获胜):
1.可以通过计算所有堆的nim-sum,nim-sum!=0先手胜,反之后手胜。
2.可以用计算后的nim-sum分别与所有堆的元素进行异或操作,如果得到结果小于原来堆的元素,则为可选操作。
我们这里有两个问题:
一、有n堆纸牌,每堆有若干张,轮流取,每次可以从某一堆取若干张,谁取完就获胜。经典的尼姆博弈。
二、有n堆纸牌,每堆有若干张,轮流取,每次可以从某一堆取若干张,谁取完就输掉。变形,把胜负条件反转一下。
我们接下来分析一下,设数量为1的堆为孤单堆,数量大于1的为充裕堆。令sum=a1^a2^...an。
设置状态①S0态,sum!=0&&充裕堆=0(奇数个为孤单堆)。
②T0态,sum=0&&充裕堆=0(偶数个孤单堆)。
③S1态:充裕堆=1,必胜态(无论问题一还是二都是必胜,可以通过对富裕堆取得只剩一张牌或者全部取完,改变剩余n-1个孤单堆的奇偶性)。
④S2态,sum!=0&&充裕堆>=2。
⑤T2态,sum=0&&充裕堆>=2。
以上5种状态已经涵盖了所有可能出现的情况。同时我们也可以得出几个个定理:
a)S2态会进入T2态。(富裕堆不可能一次从2变为0,所以S2可能进入S1或T2,S1是必胜态,不可能让给对手,所以进入T2)
b)T2态只能进入S2态或S1态。
c)S1态可以任意选择进入S0或者T0。
d)S0只能进入T0,T0只能进入S0。
先来看问题一:
这里S0态是必胜态,T0态是必败态。
那么我们推理一下S2和T2的胜负性质,S2->T2->S2->....->T2->S1->T0->S0->....->T0->S0->T0(全部为0)。
从上面我们可以看到如果自己是T2态那么一定会把S1态让给别人,又因为S1态可以任意选择S0或者T0态,所以S1态必胜。所以S2是必胜态,T2是必败态。
接下来看问题二:
这里S0态是必败态,T0态是必胜态。
同理,我们推理一下S2和T2,S2->T2->S2->....->T2->S1->T0->S0->....->T0->S0->T0(全部为0)。
我们会发现如果自己是T2态就会把S1态让给别人,S1态必胜。所以S2是必胜态,T2是必败态。(好吧感觉完全是重复了一遍- -、)
这恰恰反应了在两个问题中“除了S0和T0的胜负性质发生了变化,S1、S2永远是必胜态,T2永远是必败态。”,所以除了每堆都是1时的特判,其他时候判断都是一样的。
结论二(取完失败):
1.如果每堆都是1,则判断奇偶,奇数后手胜,偶数先手胜。
2.其他情况下如果nim-sum!=0则先手胜,反之后手胜。
例题一:
HDU 1850 Being a Good Boy in Spring Festivaly
题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=1850
题目大意:有m堆牌,两个人先后取某堆中的任意(不少于一)张牌,最后取完者胜,问先手取胜第一次取牌有多少种取法。
解题思路:典型的尼姆弈。关于有多少种胜利的取法,我们只需要判断一下在某一堆m张牌之后是否会使得a1^a2^a3.....^an=0成立,可以通过使用异或的性质,比如取第一堆,只要判断a2^a3.....^n<a1是否成立即可,把每一堆都遍历一遍就可找出答案。
代码:
1 #include<cstdio> 2 int a[105]; 3 4 int main(){ 5 int n; 6 while(scanf("%d",&n)&&n){ 7 int sum=0;//a[1]~a[n]的异或值 8 for(int i=1;i<=n;i++){ 9 scanf("%d",&a[i]); 10 sum^=a[i]; 11 } 12 int ans=0; 13 for(int i=1;i<=n;i++){ 14 sum^=a[i];//a^b^b=a^0=a 15 if(sum<a[i]) 16 ans++; 17 sum^=a[i]; 18 } 19 printf("%d ",ans); 20 } 21 }
例题二:
HDU 1907、HDU 2509
题目大意:两题除了输入输出一模一样,尼姆博弈变形,最后取完所有物品的人不是获胜而是失败,判断最后获胜的人。
解题思路:就是我在开头介绍的问题二的类型,直接套结论:
①所有堆个数都为1时,若堆数为奇数则后手胜,若堆数为偶数则先手胜。
②其他情况,所有堆的异或值sum!=0那就先手胜,反之后手胜。
代码:
1 #include<cstdio> 2 int a[50]; 3 4 int main(){ 5 int T; 6 scanf("%d",&T); 7 while(T--){ 8 int n; 9 scanf("%d",&n); 10 int sum=0,count=0; 11 bool flag=false; 12 for(int i=1;i<=n;i++){ 13 scanf("%d",&a[i]); 14 if(a[i]==1) 15 count++; 16 sum^=a[i]; 17 } 18 if(count==n) 19 flag=(count%2==0); 20 else 21 flag=(sum!=0); 22 if(flag) 23 printf("John "); 24 else 25 printf("Brother "); 26 } 27 }