6

Nim游戏的必胜策略和Xor运算的神奇应用

 2 years ago
source link: https://www.physixfan.com/nim%e6%b8%b8%e6%88%8f%e7%9a%84%e5%bf%85%e8%83%9c%e7%ad%96%e7%95%a5%e5%92%8cxor%e8%bf%90%e7%ae%97%e7%9a%84%e7%a5%9e%e5%a5%87%e5%ba%94%e7%94%a8/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Nim游戏的必胜策略和Xor运算的神奇应用

作者: physixfan

上一篇日志里介绍了Nim游戏,他的必胜策略可不是那么好想的。这个游戏貌似很久以前就已经有了,可是必胜策略直至20世纪初才被哈佛大学的一个叫做Charles Leonard Bouton的数学家找到,可见其思维难度;可是,这个必胜策略却只要由一个运算就搞定了:Xor(异或)运算,可见Xor运算之神奇。没有好好学过程序设计的人估计对Xor运算不甚熟悉,更不可能知道他的神奇应用了,因此我先说一说Xor运算。

Xor运算是位运算的一种,和And、Or运算类似,假如a、b都是布尔变量,则a Xor b被定义为:a、b相异则为真(所以中文名字叫做异或),a、b相同则为假。其真值表为:1Xor0=1, 0Xor1=1, 1Xor1=0, 0Xor0=0。众所周知,位运算也可以用于两个数之间,其定义就是把这两个数转化为二进制,然后一位一位的进行位运算。比如说1Xor4=(001)2 Xor(100)2=(101)2=5。位运算除了具有交换律、结合律这样的普通性质之外,还有几条神奇的性质。

Xor运算的神奇性质之一,就是他自己是自己的逆运算,即对于任何两个布尔变量或者数有(a Xor b)Xor b=a。这一点可以从真值表直接验证。有了这样一个性质,我们就可以把交换两个数的函数swap改进一下。大家应该都知道swap可以这么做:

void swap(int a, int b)

{a=a+b; b=a-b; a=a-b;} 

现在我们知道了Xor运算是本身的逆运算之后,就可以把上面的函数改成这个样子:(在C/C++里面把Xor表示为^)

void swap(int a, int b)

{a=a^b; b=a^b; a=a^b;}

乍一看肯定会觉得这个交换函数写的非常诡异,但是仔细一看就知道其原理和刚才那个是一模一样的。而且因为计算机在执行位运算的时候肯定比加减法要快,所以用Xor写的交换函数实际上还更快呢。

这里有一个有意思的小问题:现在给你2n+1个正整数,其中有n对数和1个单独的数,(这里规定一对数的意思是这两个数相等),然后让你设计一种算法,把这个单独的数给找出来,要求时间复杂度为O(n)。比如说这2n+1个数是1 2 3 2 1,那么这个单独的数就是3。如果你的思路是依次挑出一个数然后和其余所有数比较一下看看是否相等,那就换个思路吧,因为这样的时间复杂度是O(n2)的。答案见本文末尾。

由这条性质还可以干一件很有意义的事情:当硬盘的一个部分损坏之后可以推算出来损坏部分数据!假定我们的硬盘划分成了4个区域,前三个区域用来存放真正的数据,而第四个部分则用来以防不时之需,这上面的数据定义为前三个部分的数据异或之后的结果。举个例子:假如说abc三个部分存放的数据如下:

a: 1 0 0 1

b: 0 1 1 1

c: 1 0 1 0

则第四部分根据定义便是

d: 0 1 0 0

现在假如说系统检测到硬盘的第一部分损坏了,我们就可以利用现成的数据把它给恢复出来,只需要把现有的未损坏的几个部分都异或起来就可以了!因为:a xor b xor c=d  →  a xor a xor b xor c xor d xor d=d xor d xor a  → b xor c xor d=a!就这样,Xor运算应用在了乍一看完全不相干的地方。当然,硬盘分的部分更多一点也不影响这个结论的正确性。

Xor的第二个神奇性质,是他满足消去率,即由a Xor c=b Xor c可以推出a=b,可以用上面一条性质轻松验证。这一点是And、Or运算都不能满足的,是加法减法拥有的性质。有了这样一条性质是很有用的,比如说证明Nim游戏的必胜策略就需要用到,下面我们进入Nim游戏必胜策略的介绍和证明。

因为3堆硬币的情况和N堆的策略是一样的,我就直接拿N堆说事。设这N堆硬币的数量分别为a1,a2,…,an。因为总是打Xor太麻烦,下面我就用C++的习惯用^来代替Xor。

 要知道,像Nim游戏这种博弈问题,最重要的是寻找必败态。这个必败态的的意思就是,这样一种局面摆在面前的话先手必败。其严格定义如下:1.无法进行任何移动的局面是必败态;2.可以移动到必败态的局面是非必败态;3.在必败态做的所有操作的结果都是非必败态。这个还是很好理解的吧,就是自己处在非必败态上总能移动到必败态把必败态留给对方,而对方处在必败态的话总是只能移动到非必败态,把非必败态留给自己,然后自己继续虐对方。

而对于Nim游戏,局面是必败态当且仅当所有堆硬币的数量都异或起来结果为0,即a1^a2^…^an=0!!!为了证明之,我们只要证明它满足上述必败态的三条性质即可。

第一个命题显然,最终局面只有一个,就是全0,异或仍然是0。

第二个命题,对于某个局面(a1,a2,…,an),若a1^a2^…^an!=0(不等号就用C++的习惯用!=来表示了),一定存在某个合法的移动,将ai改变成ai’后满足a1^a2^…^ai’^…^an=0。不妨设a1^a2^…^an=k,则一定存在某个ai,它的二进制表示在k的最高位上是1(否则k的最高位那个1是怎么得到的)。这时ai^k<ai一定成立。则我们可以将ai改变成ai’=ai^k,此时a1^a2^…^ai’^…^an=a1^a2^…^an^k=0。

第三个命题,对于某个局面(a1,a2,…,an),若a1^a2^…^an=0,一定不存在某个合法的移动,将ai改变成ai’后满足a1^a2^…^ai’^…^an=0。因为异或运算满足消去率,由a1^a2^…^an=a1^a2^…^ai’^…^an可以得到ai=ai’。所以将ai改变成ai’不是一个合法的移动。证毕。

就这样,一个简单而神奇的运算,就搞定了这么个让我绞尽脑汁也毫无头绪的游戏,而Xor运算的出现,又是乍一看完全与问题毫不相干!这正是Xor的奇妙之处,吸引人之处。

————————————

最后给出“找出单独的数”问题的算法:根据Xor运算是本身的逆运算的性质,只要把所有数都Xor起来就可以了!比如说,1Xor2Xor3Xor2Xor1就一定是3了。就这么简单!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK