Avatar
“”

一个兴趣使然的追梦人

▼

#题解_U687148 玄武阁的棋子博弈

【题解】U687148 玄武阁的棋子博弈(Nim 游戏完全解析)
题目速览
题目来源:洛谷 U687148 / 经典博弈论
核心规则:
有 \(N\) 堆棋子,第 \(i\) 堆有 \(A_i\) 个。
两人轮流操作,每次任选一堆并拿走任意正整数个(可全拿)。
无法合法操作(棋子全空)者输。两人均采取最优策略。
目标:判断先手是必胜(Win)还是必败(Lose)。

结论先行
如果所有堆棋子数量的按位异或和(XOR Sum)为 0,则先手必败(Lose);否则先手必胜(Win)。

\[S = A_1 \oplus A_2 \oplus \dots \oplus A_N \]

\(S \neq 0 \implies \text{Win}\)
\(S = 0 \implies \text{Lose}\)
很多同学看到这个结论的第一反应是:“凭什么用异或?凭什么异或和为 0 就是必败态?”
本篇题解将从最简单的直觉入手,带你一步步推导并严格证明这个博弈论中的经典结论。

  1. 建立直觉:从 1 堆和 2 堆看起
    遇到看似复杂的多堆问题,最有效的方法是化简。
    1.1 只有 1 堆棋子
    先手直接一次性把这一堆拿空。
    此时棋子全无,后手无法操作,先手必胜。
    1.2 只有 2 堆棋子
    设两堆数量分别为 \((A_1, A_2)\):
    情况 1:两堆数量相等,如 \((3, 3)\)
    先手无论在哪一堆拿走多少个,后手只要在另一堆拿走相同数量即可。
    后手执行的是一种绝对的“镜像模仿策略”。无论先手怎么做,后手总能把局面再次拉回两堆相等的局面。
    直到先手拿光其中一堆,后手随即拿光另一堆,先手无子可拿而落败。
    结论:两堆相等时,先手必败。
    情况 2:两堆数量不等,如 \((3, 5)\)
    先手手握主动权,只需把 5 的那堆拿走 2 个,局面立刻变成 \((3, 3)\) 丢给后手。
    此时后手变成了面对“两堆相等”的人,后手必败,即先手必胜。

核心直觉:所谓的“必败态”,本质上是一种完美的对称平衡。谁拿到平衡局面,谁动一下就会打破平衡;而对方总能通过操作再次恢复平衡。


  1. 深入本质:多堆棋子如何定义“对称”?
    当棋子变成 3 堆甚至更多堆时(如 \(2, 3, 4\)),我们很难一眼看出怎样才算“对称”。
    数学家 Charles L. Bouton 在 1901 年给出了精妙的解法:将每一堆棋子拆解为二进制数(即 \(1, 2, 4, 8 \dots\) 的砝码组合)。
    以 \(2, 3, 4\) 为例:
    \(2 = (010)_2\)
    \(3 = (011)_2\)
    \(4 = (100)_2\)
    我们按二进制位统计“1”出现的次数:
    \(2^0\) 位(个位):只有 3 的末位是 1,共 1 个(奇数)
    \(2^1\) 位(次位):2 和 3 的该位是 1,共 2 个(偶数)
    \(2^2\) 位(高位):只有 4 的该位是 1,共 1 个(奇数)
    如果某个二进制位上的 1 是偶数个,说明这个权重的“砝码”是成对成对存在的,可以视作微观上的“对称”。如果每一个二进制位上的 1 都是偶数个,整个棋局就达到了多维度的究极平衡。
    而按位异或运算(\(\oplus\))的规则,恰好就是“不进位的二进制加法”:
    相同为 0,相异为 1。
    统计某一位上 1 的个数:偶数个 1 异或得 0,奇数个 1 异或得 1。
    因此,异或和为 0,就完全等价于“每个二进制位上 1 的个数都是偶数”这一平衡状态。

  1. 严格证明:为什么起手的异或和决定胜负?
    很多同学可能会有这样的疑问:

“终局全为 0 的异或和确实是 0。但这关起手的异或和什么事?难道不能中途把局面变成别的样子吗?”
博弈论(有向无环图博弈,Impartial Game)给出了严格的状态判定准则。一个局面是必败态(P 态)还是必胜态(N 态),必须且只需满足以下三条铁律:
终局边界:无法进行任何合法移动的状态是必败态(P 态)。
P 态不可保持:从任意一个必败态出发,无论采取何种合法操作,都只能转移到必胜态(N 态)。
N 态具有可转移性:从任意一个必胜态出发,一定存在至少一种合法操作,可以转移到必败态(P 态)。
我们来严谨检验“异或和 \(S = 0\)”是否完全满足这三条性质:
性质 1:终局边界
游戏结束时,所有堆的棋子都为 0。

\[0 \oplus 0 \oplus \dots \oplus 0 = 0 \]

此时轮到谁谁就无子可拿而判负。性质 1 成立。
性质 2:异或和为 0 的局面,走一步必然打破 0
假设当前局面的异或和为:

\[S = A_1 \oplus A_2 \oplus \dots \oplus A_n = 0 \]

现在轮到某位玩家操作,按照规则,他必须从第 \(k\) 堆中拿走一部分棋子,使该堆数量从 \(A_k\) 减少为 \(A'_k\)(即 \(0 \le A'_k < A_k\))。
操作后的新异或和记作 \(S'\):

\[S' = A_1 \oplus \dots \oplus A'_k \oplus \dots \oplus A_n \]

利用异或运算的性质,\(S'\) 可以表示为:

\[S' = S \oplus A_k \oplus A'_k \]

因为当前 \(S = 0\),所以:

\[S' = 0 \oplus A_k \oplus A'_k = A_k \oplus A'_k \]

由于必须拿走棋子,故 \(A'_k \neq A_k\),因此必定有:

\[A_k \oplus A'_k \neq 0 \implies S' \neq 0 \]

结论:只要你接手的是异或和为 0 的局面,无论你动哪一堆、拿多少个,新的异或和必然不为 0。你绝不可能直接把一个异或和为 0 的局面甩给对手。性质 2 成立。
性质 3:异或和不为 0 的局面,一定能一步把它做成 0
假设当前局面异或和 \(S \neq 0\)。
设 \(S\) 在二进制表示下的最高有效位是第 \(d\) 位(即第 \(d\) 位是 1,而更高位全是 0)。
根据异或定义,所有堆在第 \(d\) 位上的 1 相加必为奇数,因此必定存在至少一堆棋子 \(A_k\),其二进制的第 \(d\) 位也是 1。
我们考虑把这堆棋子的数量修改为:

\[A'_k = A_k \oplus S \]

我们需要验证两点:
这是合法操作吗(棋子数是否严格减少)?
因为 \(A_k\) 和 \(S\) 的最高位都是第 \(d\) 位,二者在该位上都是 1。
异或之后,第 \(d\) 位变成了 0,而比第 \(d\) 位更高的位两者都是 0,异或后依然是 0。
最高位的 1 被消掉了,因此必定满足:

\[A'_k = A_k \oplus S < A_k \]

数量确实变少了,符合规则!
操作后新的异或和会变成 0 吗?

\[S' = S \oplus A_k \oplus A'_k = S \oplus A_k \oplus (A_k \oplus S) = 0 \]

确实变成了 0!
结论:只要当前异或和非 0,玩家一定能精准找到一堆棋子并减少它,使新局面异或和恰好变成 0。性质 3 成立。

逻辑闭环:为什么起手决定生死?
根据以上三条性质,整个对弈过程就变成了一个锁死的逻辑链条:

[先手面对 S ≠ 0] --(性质3: 主动变0)--> [后手面对 S = 0]
      ↑                                      |
      |                               (性质2: 被迫变非0)
      |                                      |
      +------------(不断循环)----------------+
                                             ↓
                                    [最终后手面对全 0] 
                                    (后手无法操作,输)

如果起手 \(S \neq 0\):
先手只要按照性质 3 的策略,每次都把异或和变为 0 甩给后手。
后手无论怎么挣扎,按照性质 2,操作完后异或和必然又变成非 0。
先手再次利用性质 3 调整回 0。
如此往复,每次轮到后手时异或和都必然是 0。因为棋子总数每回合递减,终局(全 0,异或和为 0)必定由后手撞上。后手必败,先手必胜。
如果起手 \(S = 0\):
先手处于被动方,无论怎么走都会把 \(S \neq 0\) 送给后手,后手接管必胜策略。先手必败。

  1. 代码实现 (C++)
    由于每一组数据的判断仅需计算所有数的异或和,代码实现极其精简。
#include <iostream>

using namespace std;

void solve() {
    int n;
    cin >> n;
    
    int xor_sum = 0;
    for (int i = 0; i < n; ++i) {
        int a;
        cin >> a;
        xor_sum ^= a; // 依次异或每一堆的数量
    }
    
    // 如果异或和不为 0,先手必胜;否则先手必败
    if (xor_sum != 0) {
        cout << "Win\n";
    } else {
        cout << "Lose\n";
    }
}

int main() {
    // 提高输入输出效率
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    
    int t;
    cin >> t;
    while (t--) {
        solve();
    }
    
    return 0;
}

复杂度分析
时间复杂度:对于每组数据,仅需遍历一次长度为 \(N\) 的数组并执行异或操作,单组复杂度为 \(\mathcal{O}(N)\)。总时间复杂度为 \(\mathcal{O}(\sum N)\),能在 1.0 秒内轻松通过规模 \(10^5\) 级别的数据。
空间复杂度:仅使用常数个辅助变量维护状态,空间复杂度为 \(\mathcal{O}(1)\)。

posted @ 2026-10-16 11:51  hermanO  阅读(37)  评论(0)    收藏  举报