本文始发于个人公众号:TechFlow,原创不易,求个关注
今天是算法与数据结构专题26篇文章,我们来看看一个新的博弈论模型——Nim取子问题。
这个博弈问题非常古老,延续长度千年之久,一直到20世纪初才被哈佛大学的一个数学家找到解法,可见其思维的难度。但是这个问题本身却很有意思,推导的过程更是有趣,哪怕你没有多少数据基础也一定可以看明白。
Nim取子问题
这个问题的题面是这样的,我们有3堆石子,有A和B两个人轮流从其中的一堆取石子。规定每个人每次最少取1颗,最多可以取完当前堆,无法继续拿取石子的人落败。请问如果你是先手,你有必胜策略吗?
根据我们之前分析威佐夫博弈问题的套路,我们需要先来分析一下问题,找到一些典型的局面。比如说(0, 0, 0)对于先手来说一定是必败的,同理,对于一个(0, n, n)的局面,也一样是必败的。因为不论先手怎么取石子,后手只需要在另外一堆石子当中如法炮制,那么留给先手的依然是一个(0, n, n)的局面。在博弈论问题当中,我们通常会将先手必败的局面称为奇异局势。
那么这些奇异局势之间有没有什么关联呢?我们能不能找到这些局面之间的联系或者是公式呢?
我们光是靠脑子想或者是用纸笔去罗列我们所能想到的奇异局面是很难想出来的,不然也不会困扰人们长达一千多年了。但是这个问题的谜底却又如此简单,简单到让人不可思议。
首先,我们先来思考一个问题,这个问题之所以复杂,根本原因在于石子有3堆,而不是两堆。如果石子有两堆,那么就很容易了,先手除非面临两堆石子相等的情况,否则必胜。因为它可以通过拿取石子留下两堆一样的给后手,这样不论后手如何拿取,先手只需要在另一堆当中采取同样的操作,就必然可以给后手留下奇异局势。这和我们刚才分析的(0, n, n)的局面是一样的。
但是题目明确说了是3堆而不是两堆,我们不禁就开始设想起了一个问题,我们能不能想到一种策略,使得可以将三堆石子”转化“或者是看成是两堆石子呢?这样我们就可以非常容易地判断石子的输赢情况了。
解法分析
明明是3堆石子,怎么看成是两堆呢?怎么看都是自说自话,但如果你对二进制熟悉的话,你会发现这个问题可能并不是不可能的。
是的,二进制就是天生的二维“生物”,在二进制的世界当中,一切都只有两种,0和1。所以从直观上我们会觉得,也许可以将石子的数量和二进制取得关联。也许这样的关联会有助于我们找到解法。剩下的问题就成了,这个关联究竟是什么?
我们来思考另外一个问题,对于一堆石子来说,我们取走一个数量,石子的数量会减少,这是显而易见的。体现在石子的总数上,就是表示这堆石子数量的数字,减去了另外一个数字。这个是减法的操作,小学生都知道。但是小学生不知道的是,减法在二进制当中是怎么进行的,或者是它有什么规律呢?
我们先不急着回答,先来仔细分析一波。首先,减数和被减数都可以化作是二进制,也就是若干个1和0组成的数字。我们假设减数每一个为1的二进制位对应的被减数的值也是1,那么这个减法会进行得非常顺利。对应的就是从被减数当中移除掉若干个1的过程。
举个例子,被减数是9,减数是1。我们都知道9写成二进制是1001,而1的二进制是1。所以被减数减去减数的值为8,也就是1000,可以看成是1001移除了末尾的1。
如果减数存在二进制位被减数为0,比如10 - 3的情况,10的二进制是1010,3是11。很明显3的第0位是1,而10是0,这种情况下怎么办?首先,我们先把3和10当中都是1的二进制位去除。剩下的就是1000 减去 1,那么我们可以先把1000 减1 变成111,这样就回到了上面说的第一种情况,完成减法之后再加回来,所以得到的结果就是111,这其实就是一个向高位借位的过程。纵观整个减法的计算过程,其实就是被减数当中二进制位变化的过程,减去某一个数,等价于将被减数当中若干个0变成1,1变成0。
结合二进制,我们可以想到一种策略。就是统计这3个数所有的二进制位,由于我们有3个数,所以每一个二进制位最多有3个1,最少有0个1。如果每一位的1的数量和都是偶数,也就是不是0就是2的话,那么这一定是一个奇异局面。
举个例子,比如[10, 8, 2]是一个奇异局面,我们把它们写成二进制。10的二进制是1010,8的二进制是1000,2的二进制是10。所以我们可以发现这三个数的二进制位加起来,第1、2、3位都出现了两个1。这个时候先手不论如何操作,后手只需要保证剩下的三个数的二进制位维持这个特性即可。这样做可以保证最后一次拿取结束之后,给先手留下[0, 0, 0]的局面。本质上来说,它的原理和两堆石子的时候是一样的,只不过转化了一种形式。
举个例子,比如我们从10当中拿走3颗石子,得到(7, 8, 2),我们观察二进制位分别是111, 1000, 10。会发现每一位1的数量从低到高分别是[1, 2, 1, 1]。所以我们可以从1000拿取3个石子,保证留下的数量是101,也就是5。这样剩下的1的个数就是[2, 2, 2],依然是偶数。所以先手不论如何拿,后手都可以保证一定可以让留下的数字在二进制上保持偶数,先手一定必败。在不满足这个条件的局面当中先手一定必胜,因为先手可以在第一次通过拿取掉多余的1,保证留下一个必败的局面给后手。
这也是这题的解法,即通过二进制位来判断是否先手必胜。我们要判断每个二进制位当中出现的1的次数和是否是偶数,可以通过位运算的亦或来完成。在亦或操作当中,对每一个二进制位进行计算,奇数为1,偶数为0。所以我们只需要计算一下这三堆石子亦或之后的结果是否为0,就可以知道是否每一个二进制位的1的数量是否都是偶数了。
我们写成代码非常简单,我们通常用^这个符号表示亦或运算,那么代码只需要一行:
def win_or_lose(a, b, c):
return (a ^ b ^ c) == 0
推广以及证明
这里还没有结束,我们同样可以将3堆石子的局面推广到n堆,不管游戏当中玩家面临的是多少堆石子,这个结论依然都是成立的。这个成立的原因我们很容易想明白,为了严谨起见,我们可以用博弈问题常用的证明套路来证明一下。
在一个博弈问题当中,如果存在奇异局面,也就是必败局面,那么一定满足三个条件。第一个条件是无法进行任何操作的局面是奇异局面。第二个条件是可以移动到奇异局面的局面是非奇异局面。第三个条件是在奇异局面当中所作的任何操作得到的都是非奇异局面。
只要能够证明这三点,就可以证明我们的思路是正确的。
对于第一点毋庸置疑,所有石堆都没有石子的时候无法移动,这是必败状态。
我们来看第二个条件,我们假设这n堆石子的数量是a1, a2, ... an。如果当前局面是非奇异局面,根据我们的理论,那么a1 ^ a2 ^ a3 ^... ^an > 0。也就是说存在某个二进制位1的数量是奇数。
我们假设a1 ^ a2 ^ a3 ^... ^an = k,那么必然可以找到一个ai, 使得它的二进制表示在k的最高位上是1,因为k的所有二进制的1都是从这n个数当中来的,所以这样的ai一定存在。那么我们可以继续推导得到:ai ^ k < ai。因为最高位的1经过亦或之后变成了0,所以亦或操作之后一定是减小的。我们令p = ai ^ k,我们在a1^a2^a3^...^an = k 的等式两边同时亦或ai,可以得到a1 ^ a2 ^ ...^ai-1^ai+1^...^an = k ^ai,所以a1 ^ a2 ^ ...^ p ^...an = 0。
第三个条件也很好证明,因为如果当前是必败局面,也就是说a1 ^ a2 ^ ... ^ an=0。我们假设我们将an转变成了p之后依然有a1 ^ a2 ^ ... ^p=0, p < an。我们在等式两边同时亦或上p和an,可以得到:an ^ p = 0,也就是说p = an。这与p < an矛盾,所以不存在这样的转化使得奇异局面操作之后仍然是奇异局面。
这样我们就从数学上证明了这个推理的正确性,实际上已经有人对Nim取子问题有过深入的研究,这也是一个已经得到过证明的定理,叫做Bouton定理。定理的内容是先手可以在非平衡的Nim博弈中取胜,而后手可以在平衡的Nim博弈中取胜。这里的平衡就是指的是所有二进制位1的数量是偶数。
那么我们写出代码也非常简单:
def win_or_lose(nums):
ret = 0
for i in nums:
ret ^= i
return ret == 0
总结
到这里,关于Nim博弈的问题就讲完了。通过亦或操作去判断的解法真的是非常简单,但是这其中的推导过程想明白却不容易。我看过很多博客,都是直接给出的亦或这个结论,很少能够看到详细的推导过程。直接记住结论是简单的,但也很容易忘记,只有亲自推导一遍,才会明白亦或这个神奇的操作是怎么来的,为什么它可以解决Nim博弈的问题。
在整个思考推理和证明的过程当中,我们大量使用了亦或这个位运算操作,如果对它不熟悉的同学可能会看起来有些困扰。建议可以先了解学习一下二进制当中亦或的性质之后再来阅读本文,效果会更好。
目前为止,我们已经介绍完了巴什博奕、威佐夫博弈和Nim博弈这三种相对比较简单的博弈模型。在后续的文章当中,我们将会继续深入博弈论这个问题,一起去研究更加困难的博弈论问题,看看在复杂的场景当中,我们怎么样寻找奇异状态。
文章就到这里,如果喜欢本文,可以的话,请点个关注,给我一点鼓励,也方便获取更多文章。
本文使用 mdnice 排版