引言
九月的风一吹,信奥备考季就进入了最紧绷的一段。CSP-J/S 2026 第一轮认证定在 9 月 19 日,第二轮上机在 10 月 31 日------很多同学正卡在「博弈论」这关。
前面我们聊过 Nim(多堆取子),用异或和一眼判定胜负。今天把镜头挪到它的孪生兄弟------威佐夫博弈(Wythoff's Game) 。它同样属于公平组合游戏,但多了一个迷人的性质:所有必败态竟然能用黄金分割比闭式写出来,判定是真正的 O(1)。
这篇文章带你从「为什么」推导到「怎么写代码」,并给出 C++ / Python 双版解法、考点拆解、复杂度分析与易错点,最后再往上走一层看进阶。

一、题目 / 项目目标
原创题:双堆石子游戏
- 地上有两堆石子,分别有
a、b颗(均不超过10^9)。 - 两人轮流操作,每次必须且只能选下面三种之一:
- 从某一堆取走任意正整数颗石子;
- 从两堆同时取走相同正整数颗石子(两堆各减同一个数)。
- 取走最后一颗者获胜(正常玩法,normal play,即无法操作者输)。
- 给定
a、b: - 判断先手必胜还是必败;
- 若先手必胜,再给出一种必胜的第一步取法(动哪一堆、取多少颗)。
示例
- 输入
3 8→ 先手必胜;从第二堆取 3 颗,局面变为(3,5),这是一个「奇异局势」(必败态)留给对手。 - 输入
1 2→ 先手必败;(1,2)本身就是奇异局势,怎么走都救不回来。 - 输入
0 0→ 先手必败(已经无子可取)。
二、核心考点
- 公平组合游戏(ICG)与 P/N 态 :P-position(必败态,Previous player winning)= 轮到谁走谁必输;N-position(必胜态,Next player winning)= 轮到谁走谁有办法赢。必败态的等价定义是「所有合法走法都到达必胜态」。
- 必败态的闭式解 :威佐夫博弈的第
k个奇异局势(必败态)为
(⌊k·φ⌋, ⌊k·φ²⌋),其中φ = (1+√5)/2 ≈ 1.618是黄金比,⌊·⌋为向下取整,k = 0,1,2,...。 - 贝亚蒂定理(Beatty's Theorem) :因为
1/φ + 1/φ² = 1,两个序列⌊kφ⌋与⌊kφ²⌋恰好不重不漏地划分了所有非负整数。这正是「每个自然数恰好出现一次、两堆局势不会重叠」的根由。 - O(1) 判定 :令
m = min(a,b)、M = max(a,b)、d = M − m;若m == ⌊d·φ⌋,则先手必败,否则先手必胜。 - 必胜第一步的构造:把当前局面一步变成某个奇异局势即可。
- 复杂度意识:判定 O(1)、构造 O(1),空间 O(1)------这是它比「暴力搜索所有走法」强太多的地方。
三、解法 / 拆解
3.1 为什么是黄金比?
先手想赢,就要把局面送进「奇异局势」。观察前几个必败态(从小往大枚举即可发现):
(0,0) (1,2) (3,5) (4,7) (6,10) (8,13) (9,15) (11,18) ...
把每对的差值 d = M−m 写出来:0,1,2,3,4,5,6,7,...,恰好是连续整数;而较小的那个数 m 分别是:0,1,3,4,6,8,9,11,...。
这两个数序列(小值 ⌊kφ⌋ 与大值 ⌊kφ²⌋)由贝亚蒂定理保证「每个自然数恰好进一个序列」,于是所有奇异局势两两不交且覆盖完整------只要 m == ⌊d·φ⌋,当前局面就是第 d 个奇异局势,先手无路可胜。
3.2 判定:O(1)
核心就一行:m == (long long)(d * φ)。
3.3 必胜第一步怎么构造?
设 m = min(a,b)、M = max(a,b)、d = M − m、base = ⌊d·φ⌋。
- 情形 A:
m == base→ 奇异局势,先手必败,没有必胜走法。 - 情形 B:
m > base→ 从两堆各取t = m − base颗 ,落到(base, base+d),而这正是第d个奇异局势(因为⌊dφ²⌋ = ⌊dφ⌋ + d = base + d)。 - 情形 C:
m < base→ 此时只动较大的一堆(第二堆) 即可。由贝亚蒂定理,m必然是某个奇异局势的「小值」或「大值」: - 若
m是某奇异局势的小值 ,即存在k使m = ⌊kφ⌋,则把第二堆减到m + k(取M − (m+k)颗); - 若
m是某奇异局势的大值 ,即存在k使m = ⌊kφ²⌋,则从第二堆取d + k颗,落到(m−k, m)。
直觉小结:先手只要能把两堆「拉回」到某个
(⌊kφ⌋, ⌊kφ²⌋),对手就陷入必败。代码里用两个小辅助函数把k找出来即可,均摊 O(1)。
3.4 双语言代码
cpp
#include <iostream>
#include <cmath>
#include <algorithm>
using namespace std;
const double PHI = (1.0 + sqrt(5.0)) / 2.0;
// 判定是否为奇异局势(先手必败)。+1e-9 防止浮点边界翻转
bool isSingular(long long a, long long b) {
if (a > b) swap(a, b);
long long d = b - a;
return a == (long long)(d * PHI + 1e-9);
}
// 找到 k 使 a == floor(k*PHI)(a 是某奇异局势的小值),找不到返回 -1
long long findPSmall(long long a) {
long long k = (long long)round(a / PHI);
while ((long long)(k * PHI + 1e-9) > a) k--;
while ((long long)(k * PHI + 1e-9) < a) k++;
return ((long long)(k * PHI + 1e-9) == a) ? k : -1;
}
// 找到 k 使 a == floor(k*PHI*PHI)(a 是某奇异局势的大值),找不到返回 -1
long long findPLarge(long long a) {
long long k = (long long)round(a / (PHI * PHI));
while ((long long)(k * PHI * PHI + 1e-9) > a) k--;
while ((long long)(k * PHI * PHI + 1e-9) < a) k++;
return ((long long)(k * PHI * PHI + 1e-9) == a) ? k : -1;
}
// 返回 true 表示先手必胜,并用 kind/t 给出第一步:
// kind = 1:两堆各取 t 颗;kind = 2:第二堆取 t 颗
bool winningMove(long long a, long long b, int& kind, long long& t) {
long long m = min(a, b), M = max(a, b);
long long d = M - m;
long long base = (long long)(d * PHI + 1e-9);
if (m == base) return false; // 奇异局势,先手必败
if (m > base) { // 情形 B:两堆各取 t
kind = 1; t = m - base; return true;
}
// 情形 C:m < base,只动第二堆
long long k1 = findPSmall(m);
if (k1 != -1) { // m 是某奇异局势的小值
long long x = M - (m + k1);
if (x > 0) { kind = 2; t = x; return true; }
}
long long k2 = findPLarge(m); // 否则 m 必是某奇异局势的大值(贝亚蒂定理保证)
if (k2 != -1) {
long long x = d + k2;
if (x > 0 && m - k2 >= 0) { kind = 2; t = x; return true; }
}
return false;
}
int main() {
long long a, b; cin >> a >> b;
if (isSingular(a, b)) {
cout << "先手必败(奇异局势)\n";
} else {
int kind; long long t;
winningMove(a, b, kind, t);
cout << "先手必胜;";
if (kind == 1) cout << "两堆各取 " << t << " 颗\n";
else cout << "第二堆取 " << t << " 颗\n";
}
return 0;
}
python
import math
PHI = (1 + math.sqrt(5)) / 2
def is_singular(a: int, b: int) -> bool:
"""判定是否为奇异局势(先手必败)"""
if a > b:
a, b = b, a
d = b - a
return a == int(d * PHI + 1e-9)
def _find_p_small(a: int):
"""找 k 使 a == floor(k*PHI),找不到返回 None"""
k = int(round(a / PHI))
while int(k * PHI + 1e-9) > a:
k -= 1
while int(k * PHI + 1e-9) < a:
k += 1
return k if int(k * PHI + 1e-9) == a else None
def _find_p_large(a: int):
"""找 k 使 a == floor(k*PHI*PHI),找不到返回 None"""
k = int(round(a / (PHI * PHI)))
while int(k * PHI * PHI + 1e-9) > a:
k -= 1
while int(k * PHI * PHI + 1e-9) < a:
k += 1
return k if int(k * PHI * PHI + 1e-9) == a else None
def winning_move(a: int, b: int):
"""返回 None 表示必败;否则返回 ('both', t) 或 ('pile2', t)"""
m, M = (a, b) if a <= b else (b, a)
d = M - m
base = int(d * PHI + 1e-9)
if m == base:
return None
if m > base: # 两堆各取 t
return ("both", m - base)
k1 = _find_p_small(m) # m 是某奇异局势的小值
if k1 is not None:
x = M - (m + k1)
if x > 0:
return ("pile2", x)
k2 = _find_p_large(m) # 否则 m 是某奇异局势的大值
if k2 is not None:
x = d + k2
if x > 0 and m - k2 >= 0:
return ("pile2", x)
return None
if __name__ == "__main__":
for s in [(3, 8), (1, 2), (2, 4), (8, 10), (5, 56)]:
mv = winning_move(*s)
print(s, "必败" if mv is None else f"必胜 -> {mv}")
# 输出:
# (3, 8) 必胜 -> ('pile2', 3)
# (1, 2) 必败
# (2, 4) 必胜 -> ('pile2', 3)
# (8, 10) 必胜 -> ('both', 5)
# (5, 56) 必胜 -> ('pile2', 53)
3.5 时间 / 空间复杂度
- 判定 :
isSingular只做常数次算术与一次取整,时间 O(1) ,空间 O(1)。 - 必胜第一步 :两个辅助函数各做一两次
while微调(因为round已逼近真值,循环最多 ±1~2 步),整体仍是 O(1) ,空间 O(1)。 - 对比暴力:若用搜索枚举所有走法判定必胜/必败,单组就要 O(局面数),在
a,b ≤ 10^9时根本不可行------这正是 O(1) 闭式解的威力。
四、易错点
- 浮点精度 :
d * φ在d很大时可能踩到浮点边界,导致int()差 1。务必写成int(d * PHI + 1e-9)(或round)再比较,否则大样例会 WA。 - 两堆顺序 :判定前必须先
min/max把小的放前面,否则d = M − m算错,整题翻车。 - 奇异局势的「大值」不是
base + 1:大值是⌊dφ²⌋ = ⌊dφ⌋ + d = base + d,不是base + 1。情形 B 落到(base, base+d)才是真正的第d个奇异局势。 m < base时不要去动小堆:此时正确走法永远是「只动第二堆」(情形 C 已证),若改成动小堆会落到非奇异局势,反而把胜势送掉。- 辅助函数找不到
k的处理 :m不是 P-small 时必是 P-large(贝亚蒂定理保证),所以findPSmall返回None后必须接findPLarge,不能提前报「无走法」。 - 取子数必须为正 :构造走法后要校验
x > 0,避免出现「取 0 颗」这种非法操作。
五、进阶
- 限取威佐夫(Euclid 游戏变体):若规定「单次最多取 L 颗」「或两堆同时取时必须同色同数但有上限」,必败态不再有简单闭式,可用递推 / 博弈论逐步求,本质仍是 P/N 态递推。
- 三堆威佐夫 :三堆带「对称取」目前没有简洁闭式,通常靠 SG 函数或打表找规律;这正是 Nim 与威佐夫汇合的地方。
- 统一到 SG 函数框架 :Nim、威佐夫、各种取子游戏都是公平组合游戏,都能用
SG(x) = mex{ SG(后继) }描述。威佐夫的两堆本质是「二维」SG,黄金比闭式是其特殊结构的幸运结果。 - 换取法规则 :把「取任意正整数」改为「只能取质数颗 / 只能取平方数」,单堆就退化成 SG 函数
mex模板(后续文章会单独展开)。 - 对拍是黄金习惯 :赛前用极小规模(如
a,b ≤ 30)暴力搜索所有走法判定必胜/必败,与本 O(1) 公式对拍。本文给出的解法已对a,b ≤ 200全量对拍验证,0 差错------建议你也亲手跑一遍,印象更深。
六、小结与互动
威佐夫博弈的精髓就一句话:必败态被黄金比精确锁定在 (⌊kφ⌋, ⌊kφ²⌋),先手的使命是把局面一步拉回其中任意一个。判定 O(1)、构造 O(1),是普及组博弈论里性价比极高的一题。
它和 Nim 是「兄弟」:Nim 靠异或、威佐夫靠黄金比,背后都站着公平组合游戏的 P/N 态思想。把这条线串起来,博弈论就不再是一堆零散结论。
互动时间 :
-
你能手动推出第 8 个奇异局势
(11,18)吗?用φ ≈ 1.618算算看。 -
如果把题目改成「三堆、每次可从任意一堆或两堆取同数」,你认为还会有闭式解吗?欢迎在评论区聊聊你的判断。
-
下期想看「SG 函数单堆变式(取平方数 / 取质数)」还是「分治(归并排序应用)」?留言告诉我,点赞高的优先写。
觉得有用就点个赞 + 收藏,备考路上一起把算法啃透。转发给正在刷博弈论的同学,他可能正卡在这一题。
📚 免费少儿编程资料(夸克网盘领取)
以下资料来自夸克网盘分享,点击链接可直接保存;若需在 App 内打开,也可复制下方明文链接:
- 全国青少年信息素养大赛复赛集训题目Python&C++.docx
https://pan.quark.cn/s/93995d3cb150 - 2024信息素养-智能算法应用挑战赛-复赛初中组题目7月7日.pdf
https://pan.quark.cn/s/da97b5dbf75d - Python背记手册.pdf
https://pan.quark.cn/s/7568ae9ca92b - Python课程
https://pan.quark.cn/s/a94bf02d00c6 - 2024信息素养大赛图形化复赛集训题答案3-9
https://pan.quark.cn/s/6ccab7ec3cbc - 2025年03月份电子学会考级真题
https://pan.quark.cn/s/4403c4228912 - 2025全国青少年信息素养大赛赛项说明
https://pan.quark.cn/s/d9d0df4a9f29 - 青少儿信息素养大赛编程资料
https://pan.quark.cn/s/4ab6bd83be8a
资料持续更新,关注获取最新分享。