在算法竞赛的博弈论体系中,巴什博弈、Nim 博弈只能解决特定的取石子问题,而SG 函数才是公平组合游戏(ICG)的通用解题神器!它将所有公平组合游戏转化为有向图游戏,通过简单的
mex运算求解每个状态的胜负属性,再结合 SG 定理实现多游戏组合的胜负判断。无论游戏规则如何变化,只要能抽象成有向无环图,SG 函数都能轻松破解。本文将从有向图游戏、mex 运算这些基础概念出发,一步步拆解 SG 函数的定义、性质和 SG 定理,再结合经典例题讲解实战用法,让你彻底掌握这个博弈论万能工具!下面就让我们正式开始吧!
SG 函数并非孤立的概念,它建立在公平组合游戏(ICG)和有向图游戏的基础上,而mex运算是求解 SG 函数的核心工具。在正式讲解 SG 函数前,我们先快速梳理这些必备基础,为后续学习扫清障碍。
SG 函数的适用场景是所有公平组合游戏,这类游戏满足三大核心条件:
巴什博弈、Nim 博弈都是典型的公平组合游戏,而 SG 函数是解决这类游戏的通用框架,前两种博弈模型只是 SG 函数的特殊情况。
所有公平组合游戏都可以抽象为有向图游戏,这是 SG 函数能成为通用解法的关键。有向图游戏的定义非常简单:
给定一个有向无环图(DAG),图中只有一个起点,在起点上放置一枚棋子;两名玩家轮流沿着有向边移动棋子,每次只能走一步;无法移动棋子的玩家判负。
游戏与有向图的对应关系:
由于公平组合游戏一定在有限步内结束,因此对应的有向图必然是无环的(否则会出现无限循环,违反游戏规则),这保证了 SG 函数的求解不会出现死循环。
mex(minimum exclusion,最小排斥值)运算是求解 SG 函数的基础,其定义简单易懂:
mex (S) = 不属于集合 S 的最小非负整数,其中 S 是一个非负整数集合。
简单来说,就是从 0 开始,依次找第一个不在集合 S 中的数,这个数就是 mex (S) 的结果。
在 C++ 中,我们通常用unordered_set存储集合 S,然后从 0 开始遍历,找到第一个不在集合中的数即可,这也是后续求解 SG 函数的核心代码片段。
掌握了有向图游戏和 mex 运算后,SG 函数的定义就水到渠成了。SG 函数的本质是为有向图的每个节点分配一个非负整数,通过这个整数判断该节点(游戏状态)是必胜态还是必败态。
对于有向图游戏中的任意节点(游戏状态)x,设其所有后继节点为y₁, y₂, ..., yₖ(即从 x 出发能到达的所有节点),则节点 x 的 SG 值定义为:SG(x)=mex{SG(y1),SG(y2),...,SG(yk)}
特殊情况:如果节点 x 是终止状态(无后继节点),则其 SG 值为 0,即SG(x) = 0(因为空集的 mex 值为 0)。
SG 函数的核心价值在于,通过 SG 值的大小可以直接判断对应游戏状态是必胜态还是必败态,结论极其简洁:
这个结论的证明与巴什、Nim 博弈的证明思路一致,围绕必胜态 / 必败态的核心逻辑展开,分两步即可轻松证明:
若SG(x)=0,根据 mex 运算的定义,其所有后继节点的 SG 值构成的集合中一定包含 0 以外的所有非负整数,即所有后继节点的 SG 值都≠0。这意味着:当前状态下,无论玩家采取何种操作,都会将棋子移动到SG 值≠0 的必胜态,交给对方。因此当前玩家处于必败态。
若SG(x)≠0,根据 mex 运算的定义,其所有后继节点的 SG 值构成的集合中一定不包含 0,即存在至少一个后继节点 y 满足 SG (y)=0。这意味着:当前玩家可以采取操作,将棋子移动到SG 值 = 0 的必败态,交给对方。因此当前玩家处于必胜态。
巴什博弈、Nim 博弈都是 SG 函数的特殊情况,理解这一点能让你彻底打通博弈论的任督二脉,明白为何 SG 函数是通用解法:
Nim 博弈中,每一堆有n个石子的状态,其 SG 值等于石子数 n,即SG(n) = n。因此,Nim 博弈的异或和判断规则,本质上是 SG 定理的具体应用(后续讲解 SG 定理)。
巴什博弈中,每次可取 1~k 颗石子,对于有n个石子的状态,其 SG 值为SG(n) = n % (k+1)。当SG(n)=0时必败,这与巴什博弈的结论完全一致。
这说明:经典博弈模型的结论,都是 SG 函数在特定规则下的推导结果,而 SG 函数则是覆盖所有情况的通用框架。
实际的博弈问题中,往往不是单一的有向图游戏,而是由多个独立的有向图游戏组成的组合游戏(比如 Nim 博弈是多堆石子的组合,每堆石子是一个独立的有向图游戏)。SG 定理则解决了多游戏组合的胜负判断问题,是连接单个 SG 函数和组合游戏的桥梁。
设一个组合游戏由n个独立的有向图游戏组成,其起点分别为s₁, s₂, ..., sₙ,对应的 SG 值为SG(s₁), SG(s₂), ..., SG(sₙ)。定义该组合游戏的总 SG 值为所有子游戏 SG 值的异或和:

则组合游戏的胜负判断规则为:
SG 定理的伟大之处在于:将多游戏组合的问题,拆解为多个单游戏的 SG 值求解,再通过异或和合并结果。无论子游戏的规则如何不同,只要能分别求出每个子游戏的 SG 值,再做异或和,就能判断整体的胜负。这也是 Nim 博弈能通过异或和判断胜负的根本原因 —— 每堆石子的 SG 值等于石子数,总 SG 值就是所有石子数的异或和。
SG 定理的证明基于Nim 博弈的核心逻辑和SG 函数的性质,核心思路可概括为:
整个证明过程与 Nim 博弈的证明高度相似,核心都是异或和的性质和必胜态 / 必败态的传递,这里不再展开复杂的数学推导,重点掌握应用方法即可。
求解 SG 函数的核心方法是记忆化搜索,这是因为有向图游戏的节点(游戏状态)存在大量重复计算,记忆化可以避免重复求解,大幅提升效率。
f[]存储每个状态的 SG 值,初始值设为 - 1(表示未求解);x,若已求解(f[x]≠-1),直接返回结果;否则遍历其所有后继状态,求解后继状态的 SG 值,存入集合;f[x],作为当前状态的 SG 值并返回。以下是适用于绝大多数场景的 SG 函数记忆化搜索模板,可直接套用或根据题目规则微调:
#include <iostream>
#include <vector>
#include <cstring>
#include <unordered_set>
using namespace std;
const int N = 10010; // 状态范围,根据题目调整
int f[N]; // 存储每个状态的SG值,-1表示未求解
// 存储每个状态的后继状态,或根据题目规则动态生成后继
vector<int> nexts[N];
// 求解状态x的SG值
int sg(int x) {
if (f[x] != -1) return f[x]; // 记忆化,避免重复计算
unordered_set<int> s; // 存储后继状态的SG值
for (int y : nexts[x]) { // 遍历所有后继状态
s.insert(sg(y));
}
// mex运算:找最小的非负整数不在s中
for (int i = 0;; i++) {
if (!s.count(i)) {
return f[x] = i;
}
}
}
int main() {
memset(f, -1, sizeof f); // 初始化SG值为-1
// 此处根据题目规则构建后继状态nexts[]
// ...
return 0;
} 实际解题中,无需提前构建后继状态数组nexts[],而是根据游戏的操作规则,动态生成当前状态的后继状态(比如取石子游戏中,当前状态x的后继状态是x - b[j],其中b[j]是合法的取石子数)。这是 SG 函数解题的关键,后续例题会详细讲解。
本节结合 5 道经典例题,从基础的有向图游戏到复杂的切割游戏,逐一讲解 SG 函数的实战用法,所有代码均经过验证,可直接运行,覆盖绝大多数 SG 函数的考试场景。
题目链接:https://ac.nowcoder.com/acm/problem/50616

牛客网(信息学奥赛一本通)
给定一个有 N 个节点的有向无环图,M 条有向边,K 颗棋子分别放在 K 个节点上;两名玩家交替移动棋子,每次可将任意一颗棋子沿一条有向边移动到另一个节点;无法移动棋子的玩家输掉游戏。双方均采取最优策略,问先手是否必胜?
若先手必胜输出win,否则输出lose。
这是纯有向图游戏的 SG 函数模板题,直接套用 SG 定理即可:
#include <iostream>
#include <vector>
#include <cstring>
#include <unordered_set>
using namespace std;
const int N = 2010; // 题目节点数范围
int f[N]; // 存储每个节点的SG值
vector<int> edges[N]; // 邻接表存储有向边
int n, m, k;
// 记忆化搜索求解SG值
int sg(int u) {
if (f[u] != -1) return f[u];
unordered_set<int> s;
for (int v : edges[u]) {
s.insert(sg(v));
}
// mex运算
for (int i = 0;; i++) {
if (!s.count(i)) {
return f[u] = i;
}
}
}
int main() {
cin >> n >> m >> k;
// 建图
for (int i = 0; i < m; i++) {
int a, b;
cin >> a >> b;
edges[a].push_back(b);
}
memset(f, -1, sizeof f); // 初始化SG值
int res = 0;
// 计算所有棋子所在节点的SG值异或和
for (int i = 0; i < k; i++) {
int x;
cin >> x;
res ^= sg(x);
}
// 判断胜负
if (res) cout << "win" << endl;
else cout << "lose" << endl;
return 0;
}题目链接:https://ac.nowcoder.com/acm/problem/50617

牛客网(信息学奥赛一本通)
有 N 堆石子,每堆有 Ai 个石子;每次取石子时,只能取指定的 M 种数量(B₁, B₂, ..., Bₘ);两名玩家轮流操作,无法取石子的玩家判负;小 H 为先手,问其是否有必胜策略?若有,输出字典序最小的第一步操作(堆数最小,取石子数最小)。
YES表示先手必胜,NO表示必败;YES,第二行输出两个整数,表示从第几堆取、取多少个(字典序最小)。这是取石子游戏的 SG 函数经典应用,核心是动态生成后继状态,而非提前建图:
x,其合法后继状态是x - B[j](需满足x - B[j] ≥ 0),动态遍历所有合法 B [j] 生成后继;#include <iostream>
#include <cstring>
#include <unordered_set>
using namespace std;
const int N = 15, M = 1010;
int n, m;
int a[N]; // 每堆石子的数量
int b[N]; // 合法的取石子数
int f[M]; // 存储石子数x的SG值
// 记忆化搜索求解石子数x的SG值
int sg(int x) {
if (f[x] != -1) return f[x];
unordered_set<int> s;
// 动态生成后继状态:x - b[j]
for (int j = 1; j <= m && x - b[j] >= 0; j++) {
s.insert(sg(x - b[j]));
}
// mex运算
for (int i = 0;; i++) {
if (!s.count(i)) {
return f[x] = i;
}
}
}
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
cin >> m;
for (int i = 1; i <= m; i++) {
cin >> b[i];
}
memset(f, -1, sizeof f); // 初始化SG值
// 计算总SG值
int total = 0;
for (int i = 1; i <= n; i++) {
total ^= sg(a[i]);
}
if (total == 0) {
cout << "NO" << endl;
} else {
cout << "YES" << endl;
// 枚举找字典序最小的操作
for (int i = 1; i <= n; i++) { // 优先堆数小
for (int j = 1; j <= m && a[i] - b[j] >= 0; j++) { // 优先取石子数小
// 取完后总SG值为0,即为合法操作
if ((total ^ sg(a[i]) ^ sg(a[i] - b[j])) == 0) {
cout << i << " " << b[j] << endl;
return 0;
}
}
}
}
return 0;
}题目链接:https://www.luogu.com.cn/problem/P10501

洛谷 P10501
给定一个 W×H 的矩形纸张,两名玩家轮流切割纸张:每次可横向或纵向切割,将纸张分成两个矩形,保持格子完整;切割出 1×1 格子的玩家获胜;双方均采取最优策略,问先手是否必胜?
多组测试用例,每组用例一行两个整数 W, H(2≤W,H≤200),表示纸张的宽和高。
每组用例输出WIN(先手必胜)或LOSE(先手必败)。
这是二维 SG 函数的经典应用,也是反常游戏转化为 ICG 游戏的典型案例,解题的核心是状态抽象和后继状态定义:
f[w][h]表示 W×H 矩形的 SG 值,即二维 SG 函数;sg(i,h) ^ sg(w-i,h);sg(w,j) ^ sg(w,h-j);sg(W,H)≠0则先手必胜,否则必败。#include <iostream>
#include <cstring>
#include <unordered_set>
using namespace std;
const int N = 210;
int f[N][N]; // 二维SG函数,f[w][h]表示w×h矩形的SG值
// 记忆化搜索求解w×h矩形的SG值
int sg(int w, int h) {
if (f[w][h] != -1) return f[w][h];
unordered_set<int> s;
// 横向切割:分成i×h和(w-i)×h,i≥2且w-i≥2
for (int i = 2; i <= w - 2; i++) {
s.insert(sg(i, h) ^ sg(w - i, h));
}
// 纵向切割:分成w×j和w×(h-j),j≥2且h-j≥2
for (int j = 2; j <= h - 2; j++) {
s.insert(sg(w, j) ^ sg(w, h - j));
}
// mex运算
for (int i = 0;; i++) {
if (!s.count(i)) {
return f[w][h] = f[h][w] = i; // 矩形旋转,SG值相同,优化
}
}
}
int main() {
memset(f, -1, sizeof f); // 初始化二维SG值
int w, h;
while (cin >> w >> h) {
if (sg(w, h)) {
cout << "WIN" << endl;
} else {
cout << "LOSE" << endl;
}
}
return 0;
}题目链接:https://www.luogu.com.cn/problem/P4018

洛谷 P4018
共有 n 个石子,每次只能取p^k个(p 为质数,k 为自然数,p^k ≤ 剩余石子数);October 为先手,取走最后一颗石子者胜,问其是否有必胜策略?
这类题目直接求解 SG 函数效率较低,但SG 值存在明显的规律,因此可以通过打表输出前 20 个状态的 SG 值,找到规律后直接用规律解题(无需每次求解 SG 函数):
#include <iostream>
#include <cstring>
#include <unordered_set>
using namespace std;
const int N = 100;
int f[N];
// 预处理所有合法的p^k(质数的幂),小范围即可
int valid[] = {1,2,3,4,5,7,8,9,11,13,16,17,19};
int sg(int x) {
if (f[x] != -1) return f[x];
unordered_set<int> s;
for (int y : valid) {
if (x - y < 0) break;
s.insert(sg(x - y));
}
for (int i = 0;; i++) {
if (!s.count(i)) {
return f[x] = i;
}
}
}
int main() {
memset(f, -1, sizeof f);
// 打表输出0~20的SG值,找规律
for (int i = 0; i <= 20; i++) {
cout << i << ": " << sg(i) << endl;
}
return 0;
}运行代码后,输出 0~20 的 SG 值,会发现6、12、18 的 SG 值为 0,其余均不为 0,即n 是 6 的倍数时必败,与题目结论一致。
题目链接:https://www.luogu.com.cn/problem/P4860

洛谷 P4860
共有 n 个石子,每次只能取p^k个(p 为质数,k=0 或 1,p^k ≤ 剩余石子数);October 为先手,取走最后一颗石子者胜,问其是否有必胜策略?
与例题 4 一致,打表找规律:
#include <iostream>
#include <cstring>
#include <unordered_set>
using namespace std;
const int N = 100;
int f[N];
// 预处理合法的p^k(质数或1,k=0/1)
int valid[] = {1,2,3,5,7,11,13,17,19};
int sg(int x) {
if (f[x] != -1) return f[x];
unordered_set<int> s;
for (int y : valid) {
if (x - y < 0) break;
s.insert(sg(x - y));
}
for (int i = 0;; i++) {
if (!s.count(i)) {
return f[x] = i;
}
}
}
int main() {
memset(f, -1, sizeof f);
for (int i = 0; i <= 20; i++) {
cout << i << ": " << sg(i) << endl;
}
return 0;
}运行代码后,输出 0~20 的 SG 值,会发现4、8、12、16、20 的 SG 值为 0,其余均不为 0,即n 是 4 的倍数时必败,与题目结论一致。
SG 函数的解题思路看似固定,但实际应用中容易因状态抽象错误、后继状态生成遗漏等问题出错,结合算法竞赛的实战经验,总结以下核心技巧和避坑点:
w×h,取石子游戏将状态抽象为石子数n;sg(w,h)=sg(h,w)),减少计算量。SG 函数的学习,本质是抽象思维和递归思维的锻炼 —— 将复杂的游戏规则抽象为有向图游戏,用递归的记忆化搜索求解每个状态的 SG 值。它不像巴什、Nim 博弈有固定的结论可以死记硬背,而是需要理解原理并灵活应用,这也是其成为博弈论核心考点的原因。 从巴什博弈的单堆取石子,到 Nim 博弈的多堆取石子,再到 SG 函数的通用解法,博弈论的学习始终围绕必胜态 / 必败态的核心逻辑展开。掌握 SG 函数,就意味着你拥有了破解所有公平组合游戏的万能钥匙,能轻松应对算法竞赛中绝大多数的博弈论题目。 希望本文能帮助你彻底掌握 SG 函数的原理和实战用法,在博弈论的学习中更上一层楼!后续我会继续分享算法竞赛的核心知识点,关注我,一起玩转算法!