301 Nim

Problem 301

通常的 Nim 游戏的定义是这样的:有若干堆石子,每堆石子的数量都是有限的,合法的移动是“选择一堆石子并拿走若干颗(不能不拿)”,如果轮到某个人时所有的石子堆都已经被拿空了,则判负(因为他此刻没有任何合法的移动)。

假设有三堆石子,每堆有 个,有一个函数,。如果返回0,说明双方都在最佳移动的情况下,当前手的人必输。否则,返回非零值。

比如 ,对于当前手就返回 0,不过你怎么拿,对手都能使得其中两堆个数一样,之后他模仿你的操作使得两堆个数一样成立直到 0,然后轮到他拿走剩余的一堆,你输了。

题目要求 时,求 的个数。

值是多少,有 呢?

我们定义 P-position 和 N-position,其中 P 代表 Previous,N 代表 Next。直观的说,上一次移动的人有必胜策略的局面是 P-position,也就是“后手可保证必胜”或者“先手必败”,现在轮到移动的人有必胜策略的局面是 N-position,也就是“先手可保证必胜”。更严谨的定义是: 1. 无法进行任何移动的局面(也就是terminal position)是P-position; 2. 可以移动到 P-position 的局面是 N-position; 3. 所有移动都导致 N-position 的局面是 P-position。

Bouton's Theorem 对于一个 Nim 游戏的局面 ,它是 P-position 当且仅当 ,其中 表示异或(xor)运算。怎么样,是不是很神奇?我看到它的时候也觉得很神奇,完全没有道理的和异或运算扯上了关系。但这个定理的证明却也不复杂,基本上就是按照两种 position 进行分类讨论。

根据定义,证明一种判断 position 的性质的方法的正确性,只需证明三个命题: 1. 这个判断将所有 terminal position 判为 P-position; 2. 根据这个判断被判为 N-position 的局面一定可以移动到某个 P-position; 3. 根据这个判断被判为 P-position 的局面无法移动到某个 P-position。

第一个命题显然,terminal position 只有一个,就是全 0,异或仍然是0。

第二个命题,对于某个局面 ,若 ,一定存在某个合法的移动,将 改变成 后满足 。不妨设 ,则一定存在某个 ,它的二进制表示在 的最高位上是 1(否则 的最高位那个 1 是怎么得到的呢?)。这时 一定成立。则我们可以将 改变成 ,此时

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

有了以上的定理,使得 ,就是要求

代码很简单:

public static int GetAnswer()
{
    int count = 0;
    long max = 1 << 30;
    for (long n = 1; n <= max; n++)
    {
        long n1 = n;
        long n2 = n << 1;
        long n3 = n1 + n2;
        if ((n1 ^ n2 ^ n3) == 0)
        {
            count++;
        }
    }

    return count;
}
以上是我在 2015 年 5 月完成这个题目时候的思路。计算需要耗时 3s 多,已经挺快的了。

但是这个题目只需要个数而不需要知道是哪些。仔细考虑 有某种关系,要满足题意的话,二进制需要满足一些条件。不难想到,如果连续两个 1 bits,但前面是一个 0,即连续 3 bits 是 011,那么 110001(进位的 1 最后结果是 0 或者 1 不重要),那么异或肯定不是 0,也就是说,不能有两个连续的 1 bit,或者前面必须还是 1。后者就只有全 1 一种情况了。

所以可以使用两个数组 zeros, ones 来保存可能的数量,一个表示开头是 0 的数的个数,一个表示开始是 1 的数的个数,下标对应的是数的长度。从 1 bit 开始,0 或者 1, 异或都是 0,所以初始时两个数组的第一个元素都是 1。从 ,由上面的分析,可以得到如下关系。

zeros[i+1]=zeros[i]+ones[i];
ones[i+1]=zeros[i];
实现代码
int[] zero_count = new int[30];
int[] one_count = new int[30];
zero_count[0] = 1;
one_count[0] = 1;
for (int i = 1; i < 30; i++)
{
    zero_count[i] = zero_count[i - 1] + one_count[i - 1];
    one_count[i] = zero_count[i - 1];
}

return (zero_count[29] + one_count[29]).ToString();
整个计算过程中没有包含全是 1 这个符合条件的情况,不过包含了全是 0 的情况,但是题目要求 ,后者需要去掉,所以最后返回的时候既不用减一也不用加一。