母题:
有若干堆石子,每堆石子的数量是有限的,二个人依次从这些石子堆中拿取任意的石子,至少一个(不能不取),最后一个拿光石子的人胜利。
分析:
1、我们首先以一堆为例: 假设现在只有一堆石子,你的最佳选择是将所有石子全部拿走,那么你就赢了。
2、如果是两堆:假设现在有两堆石子且数量不相同,那么你的最佳选择是取走多的那堆石子中多出来的那几个,使得两堆石子数量相同,这样,不管另一个怎么取,你都可以在另一堆中和他取相同的个数,这样的局面你就是必胜。比如有两堆石子,第一堆有3个,第二堆有5个,这时候你要拿走第二堆的三个,然后两堆就都变成了3个,这时你的对手无论怎么操作,你都可以“学”他,比如他在第一堆拿走两个,你就在第二堆拿走两个,这样你就是稳赢的
3、如果是三堆 ,我们用(a,b,c)表示某种局势,首先(0,0,0)显然是奇异局势,无论谁面对奇异局势,都必然失败。第二种奇异局势是(0,n,n),只要与对手拿走一样多的物品,最后都将导致(0,0,0)。仔细分析一下,(1,2,3)也是奇异局势,无论对手如何拿,接下来都可以变为(0,n,n)的情型。
从中我们要明白两个理论:
一个状态是必败状态当且仅当它的所有后继都是必胜状态
一个状态是必胜状态当且仅当它至少有一个后继是必败状态
有了这两个规则,就可以用递推的方法判断整个状态图的每一个结点都是必胜还是必败状态。
这里引入L . Bouton在1902年给出的定理:状态(x1,x2,x3)为必败状态当且仅当 x1 XOR x2 XOR x3=0,这里的XOR是二进制的逐位异或操作,也成Nim和。
也就是当Nim和!= 0时,先手胜利,否则失败
计算机算法里面有一种叫做按位模2加,也叫做异或的运算,我们用符号(+)表示这种运算。这种运算和一般加法不同的一点是1+1=0。先看(1,2,3)的按位模2加的结果:
1 =二进制01 2 =二进制10 3 =二进制11 (+) ——————— 0 =二进制00 (注意不进位)
对于奇异局势(0,n,n)也一样,结果也是0。
任何奇异局势(a,b,c)都有a(+)b(+)c =0。
如果我们面对的是一个非奇异局势(a,b,c),要如何变为奇异局势呢?假设 a < b< c,我们只要将 c 变为 a(+)b,即可,因为有如下的运算结果: a(+)b(+)(a(+)b)=(a(+)a)(+)(b(+)b)=0(+)0=0。要将c 变为a(+)b,只要从 c中减去 c-(a(+)b)即可。也就是取走(a(+)b)个石子。这里我们要了解异或运算的一个特点 a(+)c(+)c = a
例子:
例1 (14,21,39) ,14(+)21=27,39-27=12,所以从39中拿走12个物体即可达到奇异局势(14,21,27)。 例2 (55,81,121),55(+)81=102,121-102=19,所以从121中拿走19个物品就形成了奇异局势(55,81,102)。 例3 (29,45,58) ,29(+)45=48,58-48=10,从58中拿走10个,变为(29,45,48)。 例4 我们来实际进行一盘比赛看看: 甲:(7,8,9)->(1,8,9)奇异局势 乙:(1,8,9)->(1,8,4) 甲:(1,8,4)->(1,5,4)奇异局势 乙:(1,5,4)->(1,4,4) 甲:(1,4,4)->(0,4,4)奇异局势 乙:(0,4,4)->(0,4,2) 甲:(0.4,2)->(0,2,2)奇异局势 乙:(0,2,2)->(0,2,1) 甲:(0,2,1)->(0,1,1)奇异局势 乙:(0,1,1)->(0,1,0) 甲:(0,1,0)->(0,0,0)奇异局势 甲胜。
分类:
第一个类型:就是让其判断胜利的人,对n堆石子求异或和,根据当Nim和!= 0时,先手胜利,否则失败就能判断出来。
第二个类型:先取完者判输,统计一下所有数一下大于1的个数,并将所有数字异或一遍,若大于1的个数为0&&异或和为0||大于1的个数大于0&&异或和不为零,则先手胜,否则后手胜。
第三个类型:限制最多取的个数,例如第i堆石子共有m个,最多取r个,先对m=m%(r+1);然后在进行异或求和。再根据异或和判断输赢。
第四种类型:先手的人想赢,第一步有多少种选择。当先手必输时,很显然是0。如果先手赢,那么先手必须努力创造奇异局势,即让其剩余的石子量异或和为0,上面已经讲了当面对非奇异局势是如何转化成奇异局势。当nim游戏的某个位置:(x1,x2,x3),当且仅当其各部分的nim - sum = 0(即x1(+)x2(+)x3 = 0(也就是各部分的异或为0)) 当前位置为必败点,这对于多个堆的情况同样适用。我们首先求出所有堆异或后的值sum,再用这个值去对每一个堆进行异或,令res = x1(+)sum(sum为所有堆的异或和)。如果res < x1的话,当前玩家就从x1中取走(x1-res)个,使x1乘下res这样必然导致所有的堆的异或值为0,也就是必败点(达到奇异局势),这就是一种方案。遍历每一个堆,进行上面的断判就可以得到总的方案数。
res = x1(+)sum;其实就是除了x1之外的n-1堆异或和,a(+)b(+)c=sum;sum(+)c=a(+)b(+)c(+)c=a(+)b;
ps:注意一个必败点不可能导致另一个必败点,因为如果这样的话当前这个必败点就不是必败点了,所以这里对于每个堆的操作至多只有一种方法
可以导败必败点,如果res > x1的话就无论从这个堆取走多少都不可能导致必败点!!!
例题:
1)有n堆石子,每堆石子都有任意个(题中给),A和B轮流取,每次能从一堆里取任意个(只能从一堆里取,并且
至少取一个),然后先取完者胜,问谁胜
解:将n个数(每堆石子的数量)异或一遍,若不为0则A胜,否则B胜
代码:
// nyoj 585 #include<stdio.h> int main(){ int N,n,x; scanf("%d",&N); while(N--){ int s=0; scanf("%d",&n); for(int i=0;i<n;i++){ scanf("%d",&x); s^=x; } if(s) printf("PIAOYI "); else printf("HRDV "); } return 0; }
2)有n堆石子,每堆石子都有任意个,A和B轮流取,每次能从任意堆里取最多m个(只能从一堆里取,最多能取限制的数目个),然后先取完者胜,问谁胜
思路:将a对b+1取余,然后将所有取余后的数异或一遍,若为0则先取者输,否则先取者胜利
代码:
// nyoj 135
#include<stdio.h> int main(){ int T,N,a,b; scanf("%d",&T); while(T--){ int c=0; scanf("%d",&N); while(N--){ scanf("%d %d",&a,&b);//当前堆中有a个石子,然后每次最多只能取b个 a=a%(b+1); c^=a; } if(c) printf("Win "); else printf("Lose "); } return 0; }
3)有n堆石子,每堆石子都有任意个,A和B轮流取,每次能从任意堆里取任意个(只能从一堆里取,并且至少取一个),然后先取完者败,问谁胜
思路:统计一下所有数大于一的个数,并将所有数字异或一遍,若大于一的个数为0&&异或之后为0||大于一的个数大于0&&异或之后不为零,则A胜,否则B胜(A先取)
代码:
// nyoj 888 #include<stdio.h> int main(){ int n,m,x; scanf("%d",&n); while(n--){ int num=0;//统计数字中大于1的个数 int s=0;//求异或之后的结果 scanf("%d",&m); while(m--){ scanf("%d",&x); if(x>1) num++; s^=x; } if(num==0&&s==0||(num>0&&s!=0)){ printf("Yougth "); } else printf("Hrdv "); } return 0; }
————————————————
参考:https://blog.csdn.net/BBHHTT/article/details/80199541v
https://blog.csdn.net/dear_jia/article/details/80200170