游戏中的移动数是每个 0 左侧的总 1 的总和。(或者相反,每个 1 右侧的总 0 的总和。)
(即 11000 有 2 + 2 + 2 = 6 步,但 10100 有 1 + 2 + 2 = 5 步,因为一个 0 的右边少了一个 1)
如果游戏中的总步数为奇数,则游戏的获胜者将是第一个玩家,如果游戏中的移动数是偶数,则将是第二个玩家。
证明:
在任何给定的移动中,玩家必须选择对应的位
一个 0 紧跟在一个 1 的右边。否则,总数
如果选择了对应于不同 0 的位,则 1 会增加,
如果选择了对应于 1 的位,则会减少。这样的举动
将导致相应所选位右侧的 1
向右移动一个位置。
鉴于这一观察,每个
1 必须向右移动每一个 0;它每移动一个 0
通过消耗一招。请注意,无论选择如何
玩家在任何给定的移动上,游戏中移动的总数
保持不变。
由于 Harshdeep 已经发布了一个 correct solution 循环遍历每个位(O(n) 解决方案),我将发布一个优化的分而治之 O(log(n)) 解决方案(在 C/C++ 中),让人想起similar algorithm 计算汉明权重。当然这里用 Big-Oh 来描述算法有点可疑,因为比特数是恒定的。
我已经验证所有 32 位无符号整数的以下代码给出的结果与线性算法相同。此代码在我的机器上按顺序在 45 秒内运行所有值,而线性代码需要 6 分 45 秒。
代码:
bool FastP1Win(unsigned n) {
unsigned t;
// lo: 0/1 count parity
// hi: move count parity
// 00 -> 00 : 00 >>1-> 00 &01-> 00 ; 00 |00-> 00 ; 00 &01-> 00 &00-> 00 *11-> 00 ^00-> 00
// 01 -> 01 : 01 >>1-> 00 &01-> 00 ; 01 |00-> 01 ; 01 &01-> 01 &00-> 00 *11-> 00 ^01-> 01
// 10 -> 11 : 10 >>1-> 01 &01-> 01 ; 10 |01-> 11 ; 10 &01-> 00 &01-> 00 *11-> 00 ^11-> 11
// 11 -> 00 : 11 >>1-> 01 &01-> 01 ; 11 |01-> 11 ; 11 &01-> 01 &01-> 01 *11-> 11 ^11-> 00
t = (n >> 1) & 0x55555555;
n = (n | t) ^ ((n & t & 0x55555555) * 0x3);
t = n << 2; // move every right 2-bit solution to line up with the every left 2-bit solution
n ^= ((n & t & 0x44444444) << 1) ^ t; // merge the right 2-bit solution into the left 2-bit solution
t = (n << 4); // move every right 4-bit solution to line up with the every left 4-bit solution
n ^= ((n & t & 0x40404040) << 1) ^ t; // merge the right 4-bit solution into the left 4-bit solution (stored in the high 2 bits of every 4 bits)
t = n << 8; // move every right 8-bit solution to line up with the every left 8-bit solution
n ^= ((n & t & 0x40004000) << 1) ^ t; // merge the right 8-bit solution into the left 8-bit solution (stored in the high 2 bits of every 8 bits)
t = n << 16; // move every right 16-bit solution to line up with the every left 16-bit solution
n ^= ((n & t) << 1) ^ t; // merge the right 16-bit solution into the left 16-bit solution (stored in the high 2 bits of every 16 bits)
return (int)n < 0; // return the parity of the move count of the overall solution (now stored in the sign bit)
}
说明:
要找到游戏中的移动数,可以将问题分成更小的部分并将这些部分组合起来。必须跟踪任何给定棋子中 0 的数量,以及任何给定棋子中的移动次数。
例如,如果我们将问题分成两个 16 位的部分,则以下等式表示解决方案的组合:
totalmoves = leftmoves + rightmoves + (rightzeros * (16 - leftzeros)); // 16 - leftzeros yields the leftones count
由于我们不关心总移动,只要判断该值是偶数还是奇数(奇偶校验),我们只需要跟踪奇偶校验。
这是加法奇偶性的真值表:
even + even = even
even + odd = odd
odd + even = odd
odd + odd = even
给定上面的真值表,加法的奇偶性可以用异或来表示。
以及乘法奇偶性的真值表:
even * even = even
even * odd = even
odd * even = even
odd * odd = odd
给定上面的真值表,乘法的奇偶性可以用AND来表示。
如果我们将问题分成大小相等的部分,那么零计数和一计数的奇偶校验将始终相等,我们无需单独跟踪或计算它们。
在算法的任何给定阶段,我们都需要知道零/一计数的奇偶性,以及该解决方案中移动数的奇偶性。这需要两位。因此,让我们在解决方案中转换每两位,使高位成为移动计数奇偶校验,低位成为零/一计数奇偶校验。
这是通过这个计算完成的:
unsigned t;
t = (n >> 1) & 0x55555555;
n = (n | t) ^ ((n & t & 0x55555555) * 0x3);
从这里我们将每个相邻的 2 位解决方案组合成一个 4 位解决方案(使用 & 进行乘法,使用 ^ 进行加法,以及上述关系),然后将每个相邻的 4 位解决方案组合成一个 8位解,然后每个相邻的 8 位解变成一个 16 位解,最后每个相邻的 16 位解变成一个 32 位解。
最后,只返回移动次数的奇偶性存储在第二个最低有效位中。