今天复习了一下异或运算。
题目描述
你和隔壁老王两个人玩 Nim 取石子游戏,假设你们绝顶聪明。
Nim 游戏的规则是这样的:桌子上有n堆石子,参与游戏的两人轮流从任意一堆石子里取出任意多枚石子扔掉,可以一次性取完,不能不取,且不能同时去取其他石子堆中的石子。最后没石子可取的人就输了。
现在你是先手,且告诉你这n堆石子中每堆的数量,请设计一个程序判断你是否能赢得这场游戏?
输入与输出要求
编写一个名为test的函数,调用它需要输入一个一维数组,其中每一个整数代表每一堆石子中石子的数量。
如果对于这组数据存在先手必胜策略,函数则输出字符串“ Yes”,否则输出 “No”。
测试样例
| Input | Output |
|---|---|
| [1, 1] | No |
| [1, 2, 3] | No |
| [1, 1, 1] | Yes |
题目原理
首先献上解答本题的原理:当每堆石子数的异或和不等于0时,存在必赢策略,你必赢,反之你必输(因为题目假设你和隔壁老王都绝顶聪明,如果存在必赢策略的话你一定会按必赢策略进行游戏;同样,如果你无法保证自己一定赢那么他就能保证一定击败你)。
下面给出这一原理的证明。
原理证明
要解决这道题目我们需要用到有关异或运算(xor)的相关知识,这是攻破这道题非常有利的武器。首先我们先利用运算的基本性质,来推导两个有关异或运算的基本结论,最终证明的时候要用到。 忘记异或运算的同学可以去复习一下。
推导结论一
假如我们有若干个整数(设它们的数组为A),设它们的异或和为k(k≠0),对于k在2进制下的最高位i的1,在这些整数中显然存在奇数个整数(设它们的数组为B)满足在二进制的第i位是1(不然k在第i位上就不可能是1)。
现在我们取出B中的任意一个元素设为N,设A中排除掉N后的所有元素的异或和为x,就能得到N^x=k,即k=N^x。而我们又知道k^k=0,所以N^x^k=0,即(N^k)^x=0。
并且,由于N与k的第i位都为1,因此将它们进行异或运算后,所得结果的第i位变为0,即N^k<N。可见,若想得到N^k,只需要将N减去某个数即可。
推导结论二
若A中元素的异或和k=0。
显然,如果你把A中任何一个元素进行减小,都会导致重新异或运算的结果k'≠0。因为对一个数作减法后它的二进制表达一定会发生变化,必然会导致最终的异或运算结果发生变化。
最终证明
有了上面的两个推导结论,接下去就好办多了。为了方便,我们不妨设一个数组A,把所有石子堆中的石子数量都放在里面,并且设A中所有元素的异或和为k。
我们都知道,无论你和隔壁老王谁胜谁负,最终所有的石子都会被丢光,即最终A中所有的元素都变为0,此时显然k=0。而最终面对这一结局的显然是最后输的一方(因为你们俩是轮流丢石子的)。
因此,如果你想取得胜利,即想让隔壁老王输,那么就必须让隔壁老王去面对上述的这一结局,也就是你丢掉了游戏中最后剩余的全部石子。
该怎么办呢?其实很简单,你只需要确保每次自己都面对k≠0的局面就可以啦。即如果游戏开始时k≠0,你需要在每一次丢石子时,确保丢完之后k=0,而根据刚才我们推导的结论一,这是可以办到的。
我们接着往下分析,当每次你丢完之后,k=0,而隔壁老王根据游戏规则,不得不丢掉任意数目的石子,根据我们刚才推导的结论二,老王丢完之后k又重新不等于0了。
每次老王丢完后,你又重复上述你的操作,如此循环往复。显然,在游戏结束前,你能够确保自己每次都面对k≠0的局面,而老王永远面对k=0的局面,直至最后你拿走了最后的石子,隔壁老王面对空空如也的桌面只得认输。
而反过来,如果游戏开始的时候k=0,那么我们同样利用之前的两条推导结论可以知道你第一轮丢掉任意数量的石子后k≠0,老王就可以利用之前那种情况中原先你采用的策略来使k重新为0,进而保证他永远面对k≠0的局面,循环往复,最终让你输掉游戏。
综上,通过充分利用异或运算的性质可知,Nim游戏中你作为先手是否有必赢策略,与每堆石子中的石子数的异或和是否等于0有关,我们开始时候提到的原理得到了最终证明。
代码奉上
弄明白了解题的原理,代码写起来就很方便啦。下面是JavaScript版本的实现:
"use strict";
function test(data) {
let ans = 0;
data.forEach(num => (ans ^= num));
console.log(ans ? 'Yes' : 'No');
}
运行效果: