← 目录 / 算法文档 · 模块十九 拓展专题 / 19.2 博弈论

19.2 博弈论

两个玩家轮流做决策,谁先无法操作谁就输——不需要真的把每一种走法都试一遍,几个简单的规律就能判断谁有必胜策略。

本页目录
① 什么是博弈论问题

这里说的"博弈论",特指一类组合游戏:两个玩家轮流操作,双方都采取最优策略(不会犯错),游戏没有随机成分(不像掷骰子),信息完全公开(双方都清楚当前局面)。这类问题通常问的是:先手还是后手有必胜策略?不需要真的模拟每一种可能的走法组合,往往能找到一个简单的判断规律。

② 核心概念:必胜态与必败态

把游戏的每一种局面分成两类:

类别定义
必败态(P 态)轮到你走的时候,你必输——要么已经无法操作(直接判负),要么不管怎么走,都会走到一个必胜态(留给对手一个好局面)
必胜态(N 态)轮到你走的时候,你有必胜策略——至少存在一种走法,能走到一个必败态(把烂摊子丢给对手)
💡
两句话记住核心规律:① 没有后续可走的局面是必败态;② 一个局面是必胜态,当且仅当存在一种走法能到达必败态;一个局面是必败态,当且仅当所有走法都只能到达必胜态。这是一个天然的递归定义,可以从"没有后续可走"的局面开始,反向推导出所有局面的胜负性质。
③ 引例:Nim 游戏

Nim 游戏:有若干堆石子,两人轮流操作,每次可以选任意一堆,从中拿走任意数量(至少 1 个,可以拿走整堆)。轮到某个人时所有堆都是空的,这个人就输了。Nim 游戏有一个非常简洁的结论:

Nim 游戏必胜定理

先手必胜 ⟺ 所有石堆数量的异或和(XOR)不等于 0

换句话说:如果所有堆的石子数异或起来结果是 0,当前局面是必败态(轮到谁走谁输,只要对手不失误);只要异或和不是 0,当前局面就是必胜态。

用两组石堆验证这个结论——[1, 2, 3][1, 2, 4]

局面一:石堆 [1, 2, 3](必败态)
堆1
1
堆2
2
堆3
3
异或和 1 ⊕ 2 ⊕ 3 = 0——先手无论怎么拿,都会把局面变成异或和不为 0 的状态(必胜态),留给对手;对手再用必胜策略把局面变回异或和为 0,如此反复,先手必输。
局面二:石堆 [1, 2, 4](必胜态,找到具体的必胜走法)
堆1
1
堆2
2
堆3
4 → 3
异或和 1 ⊕ 2 ⊕ 4 = 7 ≠ 0,先手必胜。必胜走法:对每一堆 i,计算 目标值 = 总异或和 ⊕ 该堆数量,如果目标值小于该堆当前数量,就把这一堆拿到只剩目标值那么多。这里堆 3(数量 4):目标值 = 7 ⊕ 4 = 33 < 4,把堆 3 从 4 拿到 3(拿走 1 个)。拿完之后局面变成 [1,2,3]——正是上面验证过的必败态,先手把烂摊子丢给了对手!
④ 推广:SG 函数与 Sprague-Grundy 定理

Nim 游戏只是众多组合游戏中的一种。SG 函数(Sprague-Grundy 函数)能把"必胜态/必败态"这套二元判断,推广成一个更精细的数值,从而处理更一般的游戏,甚至能把多个独立的子游戏组合在一起分析。

定义 SG(state):从当前局面出发,能到达的所有下一个局面的 SG 值,取最小的没有出现过的非负整数(这个操作叫 mex,minimum excludant)。SG(state)=0 当且仅当 state 是必败态——这正好和 Nim 游戏"异或和为 0 是必败态"的结论吻合,因为单堆 Nim 游戏里 SG(n) = n(能拿到 0~n-1 中的任意数量,mex 就是 n 本身)。

以"每次最多拿 3 个"的单堆取石子游戏为例(不能不拿,也不能一次拿超过 3 个),计算 SG(0)SG(4)

n(剩余石子数)能到达的局面能到达局面的 SG 值SG(n) = mex{...}
0无(游戏结束){}0
1拿1→0{0}1
2拿1→1,拿2→0{1, 0}2
3拿1→2,拿2→1,拿3→0{2, 1, 0}3
4拿1→3,拿2→2,拿3→1{3, 2, 1}0
📌
SG(4)=0:能到达的局面是 {3,2,1},这几个数里没有出现 0,所以 mex=0——这说明剩 4 个石子时,当前操作的人是必败的(不管拿1、2还是3个,都会把局面让给一个 SG 值不为 0 的必胜态)。规律是 SG(n) = n mod 4,之后会不断循环。
🏆
Sprague-Grundy 定理:如果一个复杂的游戏可以拆解成若干个互相独立的子游戏(每次操作只能选一个子游戏进行,子游戏之间互不影响),那么整个复杂游戏的 SG 值,等于所有子游戏 SG 值的异或。这正是 Nim 游戏结论的推广——普通 Nim 游戏的每一堆就是一个独立子游戏,每堆的 SG 值就是它自己的石子数,异或起来正好是 Nim 游戏的判断依据。
⑤ 完整代码
C++ · Nim 游戏判断 + 通用 SG 函数
1// Nim 游戏:直接异或所有堆,判断先手是否必胜
2bool NimFirstWins(const vector<int>& piles)
3{
4 int x = 0;
5 for (int p : piles) x ^= p; // ★ 全部异或起来
6 return x != 0;
7}
8
9// 通用 SG 函数:以"每次最多拿 k 个"的单堆游戏为例
10int sg[MAXN];
11void ComputeSG(int n, int k)
12{
13 for (int i = 0; i <= n; i++)
14 {
15 bool vis[MAXN] = {}; // 记录"能到达的局面"里出现过哪些 SG 值
16 for (int take = 1; take <= k && take <= i; take++)
17 vis[ sg[i - take] ] = true;
18 int m = 0;
19 while (vis[m]) m++; // ★ mex:找最小的没出现过的非负整数
20 sg[i] = m;
21 }
22}
💡
多个独立子游戏的组合,直接异或各自的 SG 值:算出每一堆(或每个子游戏)的 sg[i] 之后,如果整个游戏由多个独立部分组成,把它们的 sg 值异或起来,结果为 0 就是必败态,否则是必胜态——这一步和 NimFirstWins 里的异或操作是同一个原理,Nim 游戏只是"每堆的 SG 值等于堆里的石子数"这个特殊情形。
⑥ 常见陷阱
把"谁先无法操作谁赢"和"谁先无法操作谁输"搞反:本节默认的是"无法操作判负"(标准游戏规则),但有些题目是反过来的("无法操作判胜",比如某些取石子游戏的变体)。规则不同,必胜必败的判断条件也会不同,做题前一定要看清楚题目具体怎么定义输赢。
mex 的计算不是"最大值 + 1",而是"最小缺失值":比如能到达的局面 SG 值是 {0, 1, 3},mex 不是 4,而是 2(因为 2 没有出现过,是最小的缺失值)。跳过 2 直接从 {0,1,3} 推出 mex=4 是常见的计算错误。
把 SG 定理用在"非独立"的子游戏组合上:Sprague-Grundy 定理要求各个子游戏之间完全独立(一次操作只能影响其中一个子游戏)。如果子游戏之间有关联(比如某个操作会同时影响两堆),就不能简单地把各自的 SG 值异或起来,需要重新分析整个复合游戏的状态转移。
🏆
接下来:19.3 节的数论进阶会回到 1 模块数论基础的话题,补充扩展欧几里得算法和乘法逆元——这两个工具在涉及模运算的除法、同余方程求解时经常用到,也是数论类问题里常见的进阶技巧。