算法知识图谱总览 / 博弈论族 / Nim游戏

Nim游戏

P0intermediatedeepG-E-6

What · 是什么

n 堆石子轮流取任意堆任意多枚,Nim 和(全部堆的异或)为 0 则先手必败(P 态),非 0 则先手必胜(N 态);公平组合博弈的基础模型

Nim 游戏:共 n 堆石子,第 i 堆有 a_i 枚。两名玩家轮流操作,每次选取任意一堆,取走该堆中的任意正整数枚石子。无法操作(所有堆为空)的玩家输。结论(Bouton 定理):局面先手必胜(N 态)当且仅当 Nim 和 s=a_1⊕a_2⊕⋯⊕a_n≠0;先手必败(P 态)当且仅当 s=0。必胜策略:找到最高位的 1(s 的最高位),选一堆 a_i 使其最高位为 1,将其改为 a_i⊕s(该值 < a_i),使新异或和为 0。

Why · 为什么

直觉 · Intuition

异或像是『二进制的奇偶校验』。两个相等的堆互相抵消(同堆策略——对手拿什么你拿什么),异或和恰好刻画了这个抵消后的残余。残余为 0 说明局面完全'对称',后手永远跟一手就能赢;残余非 0 先手只需一步把残余清零,把'对称'交给对手。

原理 · Principle

三性质完备:①终止态(全 0)异或和为 0(P 态)。②异或和非 0 时存在合法操作把它变 0:设 s 最高位为 k,取最高位含 1 的一堆 a_i,因 a_i≥2^k 而 a_i⊕s < a_i(s 使 k 位翻转为 0,更高位不变),拿走 a_i−(a_i⊕s) 个即可。③异或和为 0 时任何合法操作(把某一堆 a_i 减小为 a_i')必使新异或非 0:因 s'=s⊕a_i⊕a_i'=a_i⊕a_i'≠0(a_i≠a_i')。①②③ 恰为博弈图 P/N 标记的归纳条件。

历史 · History

Nim 起源于中国古代游戏('捡石子' Jianshizi 类似)。Charles L. Bouton 于 1901 年首次发表完整数学分析(含异或策略),并命名 Nim。之后 Sprague 与 Grundy(1935/1939)把结论推广为 SG 理论,证明所有公平组合游戏都等价于某种单堆 Nim。Nim 因此是组合博弈论的基石模型,被 OI Wiki、CPH 与 CP-Algorithms 收录。

How · 实现

伪代码

# 判定先手胜负
def nim_win(piles):
  s = 0
  for a in piles: s ^= a
  return s != 0

# 构造必胜走法:返回 (堆下标, 取走后该堆大小) 或 None
def nim_move(piles):
  s = 0
  for a in piles: s ^= a
  if s == 0: return None
  for i, a in enumerate(piles):
    if (a ^ s) < a:
      return (i, a ^ s)  # 从第 i 堆取走 a-(a^s) 枚

Python 实现

def nim_win(piles):
    """返回当前局面是否为先手必胜(正常规则)。"""
    s = 0
    for a in piles:
        s ^= a
    return s != 0


def nim_move(piles):
    """返回必胜策略中的一步:元组 (堆下标, 操作后该堆剩余)。
    若已是 P 态(先手必败)返回 None。
    """
    s = 0
    for a in piles:
        s ^= a
    if s == 0:
        return None
    for i, a in enumerate(piles):
        if (a ^ s) < a:
            return (i, a ^ s)
    return None


def nim_strategy(piles):
    """演示:输出先手操作,并让后手(模拟最佳)应对后判断结果。
    仅用于教学演示。
    """
    if not nim_win(piles):
        return "P 态:先手必败"
    i, rest = nim_move(piles)
    return f"先手取第 {i} 堆,使其变为 {rest}"


if __name__ == "__main__":
    # 经典判定
    assert nim_win([1, 1]) is False        # 对称局面,后手跟手必胜
    assert nim_win([1, 2, 3]) is False     # 1^2^3 = 0
    assert nim_win([2, 5, 4]) is True      # 2^5^4 = 3
    assert nim_win([3]) is True
    assert nim_win([]) is False
    assert nim_win([1, 1, 1]) is True      # 1^1^1 = 1

    # 必胜走法正确性:移动后应为 P 态(异或和 0)
    for piles in [[2, 5, 4], [3, 4, 5, 6], [10, 8, 2], [7]]:
        i, rest = nim_move(piles)
        nxt = piles[:]
        nxt[i] = rest
        s = 0
        for a in nxt:
            s ^= a
        assert s == 0, (piles, nxt)

    # P 态无合法必胜走法
    assert nim_move([1, 1]) is None
    assert nim_move([1, 2, 3]) is None

    print("All tests passed.")
PyPy 提示O(n) 异或扫描即可,n=1e5、a_i=1e9 轻松通过。构造走法时 (a^s)<a 的判断等价于最高位含 1,不需要位运算技巧包装。Python 的整数异或无溢出顾虑。
时间复杂度判定 O(n);构造走法 O(n)
空间复杂度O(1)
注意 · 局限适用于公平组合游戏(双方行动集相同、完全信息、无平局、正常规则)。若规则含『不能取』约束或反常规则(最后取者输),结论需调整(见 anti-nim)。存在走法但题目若要求输出字典序最小等需要额外排序。多堆且 a_i 巨大时仍 O(n)。

易错点

例题

OJ题号难度考点链接
LuoguP21972【模板】Nim 游戏:直接判定异或和,OI Wiki 模板题https://www.luogu.com.cn/problem/P2197
AtCoderABC 278 G? / AGC 系列组合博弈7AGC 002 E Candy Piles:Nim 思想 + 阶梯/排序化归(OI Wiki 习题)https://atcoder.jp/contests/agc002/tasks/agc002_e
SPOJCOT3 Combat on a tree8树上博弈的 SG/Nim 组合综合题(OI Wiki 习题)https://www.spoj.com/problems/COT3/
步骤迷你图(节点 = 算法步骤 · 边 = 流程 · 绿色 = 起始步骤)
上游 · 本知识点依赖(前置/变体/组合/共现)

信源

✓ = 已核验 · △ = 未核验