ProjectEuler 301 游戏中的数学

来源:互联网 发布:数据名录 编辑:程序博客网 时间:2024/05/25 23:57

好久没做Euler题目了,上去直接挑了最后一题。一般说来越往后越难,但题目301却非常简单。更难得的是,经过一番思考,我发现自己能够比较完整的理解题目背后的数学和算法,所以把心得分享给大家。(有很多Euler题目,要么不会做,要么勉强做出结果,却想不出清晰的数学图像)

 

Nim是很经典的博弈游戏。有3堆石子,两人轮流从任一堆中取走至少1个石子,最后拿走剩下石子的人获胜。类似的游戏还有很多,比如21点:两人轮流从21个石子里拿走1~3个石子,也是最后拿走剩下石子的人获胜。有趣的是,这类游戏是存在必胜法的。

 

先看21点,要点是保持剩余石子数是4的倍数。我们记这些石子数的集合为A = {0, 4, 8, 12, 16, 20};并记第i次取石子后,剩余石子数为Ri。这些元素有3个神奇的特性:

1、游戏的最终状态0∈A,轮到谁,谁就输了。

2、如果Ri∈A,则任何取法都会使Ri+1A。例如无法直接从8到4,因为一次最多取3个石子。

3、如果RiA,则必有某种取法,使得Ri+1∈A。例如7 - 3 = 4。

由上述3个特性可知:

1、给对手剩余0则胜利,而0是4的倍数。

2、我方只要维持剩余石子数为4的倍数,则无论对手怎么取,必会偏离此状态。

3、无论对方怎么取,我方总有合适的取法,使剩余石子数回到4的倍数。

所以在对局双方都知道此必胜法的情况下,则输赢只跟谁先出手有关了。

 

有了上述例子,再看Nim游戏就比较好理解了。记第i次取石子后,三堆石子的个数分别为Xi, Yi, Zi。其必胜心法就是:从某堆中抽出适量石子,使得Xi⊕Yi⊕Zi = 0(此处及下文出现的⊕是二进制按位异或算符xor)。对比21点游戏,我们也试着"猜想"出3个神奇的特性:

1、根据规则,游戏的最终状态为Xi = Yi = Zi = 0,轮到谁,谁就输了。

2、如果Xi⊕Yi⊕Zi = 0,则任何取法都会使Xi+1⊕Yi+1⊕Zi+1≠0。

3、如果Xi⊕Yi⊕Zi≠0,则必有某种取法,使得Xi+1⊕Yi+1⊕Zi+1 = 0。

由上述3个特性可知:

1、给对手剩余Xi = Yi = Zi = 0则胜利,而此时恰好Xi⊕Yi⊕Zi = 0。

2、我方只要维持剩余石子数满足Xi⊕Yi⊕Zi = 0,则无论对手怎么取,必会偏离此状态。

3、无论对方怎么取,我方总有合适的取法,使剩余石子数回到Xi⊕Yi⊕Zi = 0的状态。

 

凑巧的是,这个猜想是成立的,但其正确性不再像21点游戏那样直观了。我给出描述性的证明,严格证明请参看Wikipedia。

 

先证性质2,这个简单。由于只能从单一堆中取,且至少取走1个石子。不失一般性的,假设是从Xi中取,则必定会使Xi+1的某些二进制位发生变化,而另两堆保持不变Yi =Yi+1,Zi =Zi+1,所以Xi+1⊕Yi+1⊕Zi+1的某些位必定发生改变,而使其偏离0了。(假如游戏允许从多个堆中同时取走石子,则性质2失效。)

 

再证性质3。我们可以构造性的给出一个算法,使得在任意的Xi⊕Yi⊕Zi≠0时,总可以从某堆中取走一些的石子,使得Xi+1⊕Yi+1⊕Zi+1恢复为0。例如:

Xi = 4 = (100)2

Yi = 2 = (010)2

Zi = 3 = (011)2

-------------------

Xi⊕Yi⊕Zi = (101)2

由于Xi⊕Yi⊕Zi的最高位是第三位(bit3 = 1),则三堆中必然至少有一堆的第三位也为1,任选这样的一堆,此例即X。由于异或后的值(101)2在第一位和第三位都是1,为了让其归零,就需要让Xi+1的对应位发生变化,也就是从(100)2变为(001)2。再仔细一些,会注意到Yi、Zi的第三位要么都为0,要么都为1,则Xi+1的最高位至多是第二位,所以Xi+1i,不可能出现石子不够取的情况。综上,需要从X中拿走(100)2 - (001)2 = (011)2 = 3个石子。归纳一下,此算法为:总是从与Xi⊕Yi⊕Zi具有相同最高位的任一堆中取走石子,不失一般性的,设此堆为X。希望取走石子后此堆剩余石子Xi+1 = Xi⊕(Xi⊕Yi⊕Zi)= Yi⊕Zi,则必然可以从X中取走的数目为Xi -Xi+1的石子使得Xi⊕Yi⊕Zi = 0。

 

有了这些铺垫,题目301的解法也就很直接了。只要遍历n = [1..230],数一下满足n⊕2n⊕3n = 0的n的个数即可。(答案?保密)

 

小结一下,就会发现这类游戏的特点是对局双方仅有先后手的差别。对此有个学名:无偏博弈(Impartial game),并且所有这类游戏都等价于Nim游戏(Sprague–Grundy theorem)。他们中的每一个游戏,都拥有一个特殊的状态集合。在21点中就是集合A,在Nim中,就是那些满足X⊕Y⊕Z = 0的局面。这个集合(我称作核心集)中的状态有如下3个性质:

1、终局状态属于核心集。

2、如果当前状态属于核心集,则任意合法的动作,都会使当前状态转化到核心集之外的状态。

3、如果当前状态不属于核心集,则必存在一种精心策划的合法的动作,使当前状态转化为核心集之内的状态。

 

我还要继续深入,看看是否有更快的算法呢?让我们先瞧瞧Nim游戏的核心集的样子(图1)。咋就那么像立体的Sierpinski triangle呢?看来Nim游戏的内部隐藏了某种深层次的规律性,如果能找到,说不定会大大缩短之前遍历法的计算时间。仔细读题,初始三个堆的大小正好是n的1倍、2倍、3倍。这有什么特殊含义吗?

图1,Nim核心集,x≤y≤z。

 

晚上做梦,

1、梦见数字电路设计中讲,异或和加法是非常相似的两种运算,唯一的不同就是加法考虑进位,而异或忽略进位。

2、又梦到在二进制表示下,2n就是对n左移一位的结果。

3、当我再梦到3n = n + 2n的时候就惊醒了(晕,真的再也睡不着了)。

4、题目要找合适的n,使得n⊕2n⊕3n = 0,这不就等价于n⊕2n = n + 2n嘛。

5、不考虑进位的异或结果与考虑进位的加法结果相等,也就是说加法过程中必定不会出现进位。

6、要满足4,则n的二进制表示中的相邻位必定不能出现连续的'1',否则左移一位后必定导致加法进位,也就是n & 2n = 0。

7、至此,问题已经相当明朗,就是要在给定的范围内,构造二进制表示中没有相邻的'1'的整数n。也就是要往一串'1'中插入一些'0',并数数有多少种不同的插入方式。

 

回忆一下高考时做过的各类排列组合题吧。将n个相同的小球,全部分到m个不同的盒子里,且盒子可以空着,有多少种分法?解法为使用"第二类隔板法",将球与盒看做相同的元素,排成一串。除去左右两侧的空隙,构成n + m - 1个空隙。分成m堆则需要插入m - 1个隔板,所以有种分法。( Mathematica中对应函数为Binomial[n + m - 1, m – 1] )

 

有了这些基础,计算总数就容易多了。我们按'1'在n中出现的次数进行分类,逐个类进行计算:

 

第1类,以数(1)2为基础,插入不同个数的'0',构成新的n:

插入'0'的个数

形成n的数目

0

1

2

...

...

29

 

第2类,以数(101)2为基础,插入不同个数的'0',构成新的n:

插入'0'的个数

形成n的数目

0

1

2

...

...

27

 

第3类,以数(10101)2为基础,插入不同个数的'0',构成新的n:

插入'0'的个数

形成n的数目

0

1

2

...

...

25

以此类推...

第15类,以数(101...101)2为基础,即15个'1',14个'0'为基础,插入不同个数的'0',构成新的n:

插入'0'的个数

形成n的数目

0

1

第16类,由于前15类构造的n值都小于230,而题目设定n的范围包含230,恰好230是合理的n,所以也要算上1个。其实这就是第一类的延续而已。

 

所以,最终结果就是

其中N为题目限定的2N的指数(N = 30),为向下取整。这个式子是否还能进一步化简呢?让我们把这些求和项重新排列一下,变为:

展示为表格的形式,以N = 8为例,表格中最后一行为每一列的和:

 

  

    

      

1

1

2

3

5

8

13

21

 

这个数列很眼熟吧,这就是Fibonacci数列。为什么会这么巧呢?杨辉三角(Pascal's Triangle)中的每一项都是二项式系数,且每一项都是上一行的两项之和,也就是。我们将杨辉三角以左对齐的方式写出来:

1

1

1

1

2

1

1

3

3

1

1

4

6

4

1

1

5

10

10

5

1

1

6

15

20

15

6

1

绿色对角线之和为5,蓝色对角线之和为8,红色对角线之和为13。而每一个红色的数都是其上一行蓝色绿色数值之和,所以红色对角线的和正好是绿色对角线蓝色对角线的所有数值之和。用F(n)表示第n条对角线的数值之和,于是有:

更有趣的是,这个关系式还与找零钱问题有关。考虑将x元钱分解为1元、2元纸币的方式,我们将顺序也考虑进去,即认为(1元+2元)与(2元+1元)是不同的分法:

总钱数

1

2

3

4

5

1张

1

2

   

2张

 

1+1

1+2
2+1

2+2

 

3张

  

1+1+1

1+1+2
1+2+1
2+1+1

1+2+2
2+1+2
2+2+1

4张

   

1+1+1+1

2+1+1+1
1+1+1+2
1+1+2+1
1+2+1+1

5张

    

1+1+1+1+1

分法总数

1

2

3

5

8

这张表的每一个格正好对应杨辉三角的一个数值,而各列的加和构成Fibonacci数列。

 

回到正题,有了上面的知识,我们就可以把结果中的双重求和式化简:

是否能进一步简化呢?这就要用到离散数学中最神奇也最有用的发明之一了,这就是生成函数法(Generating function)。生成函数可以将有关数列的问题转化为有关函数的问题,使得人们可以利用已有的大量分析函数的方法来分析数列。利用生成函数法,我们甚至可以处理任何种类的计数问题(counting problem)。

 

我们首先定义一个新的函数,它是一个多项式,每项的系数构成Fibonacci数列:

其中。尖括号中每一项为右侧多项式的系数。

 

由Fibonacci数列的递归定义式可知,,即

得到这个简单的公式后,只要对F(x)做幂级数展开,就可以将Fibonacci数列由递归形式转化为更为直接的解析形式(closed form)了:

其中(黄金分割率),所以Fibonacci数列的前n项之和就是两个等比数列之差:

所以,题目的最终结果竟然是简单的:

综上,本文通过分析ProjectEuler题目301的求解过程,展示了Nim游戏策略,即Nim值的构造过程。进而发现问题的解与杨辉三角、Fibonacci数列的内在联系,最终大大简化了计算过程,时间复杂度由O(N)变为O(logN)。希望读者能跟我有一样的感受,数学的美就在于那不期而遇的巧合。