SG函数:让博弈“化整为零”

引言

在算法竞赛中,博弈论题目常常让人望而生畏:两个绝顶聪明的人轮流操作,问谁赢谁输。最简单的取石子游戏(Nim 游戏)有一个漂亮的结论------异或和为 0 则先手必败,否则先手必胜。但题目稍微一变,比如"可以把一堆石子分成两堆",这个结论就不够用了。

SG 函数(Sprague-Grundy Function) 正是为了统一解决这类公平组合游戏而生的。它将复杂的博弈局面映射为一个非负整数(SG 值),然后通过 SG 定理 将多个子游戏的组合转化为 Nim 和(异或和)。掌握 SG 函数,你就掌握了解决大部分公平组合游戏的通法。

如果说 Nim 游戏是一把"钥匙",那么 SG 函数就是 "万能钥匙胚"------任何公平组合游戏,只要算出每个局面的 SG 值,就能用 Nim 和的思路判断胜负。


前置知识

在学习 SG 函数之前,你需要掌握以下基础:

  1. 公平组合游戏(ICG) 的定义:

    • 两名玩家轮流行动。

    • 任何状态下,两名玩家的合法操作集合完全相同

    • 不能行动者输。

    • 游戏一定在有限步内结束

  2. 必胜态与必败态

    • 没有后继状态的状态是必败态(P-position)

    • 存在至少一个后继状态为必败态的状态是必胜态(W-position)

    • 所有后继状态都是必胜态的状态是必败态

  3. mex 运算mex(S) 表示不属于集合 S 的最小非负整数。例如 mex({0, 2, 4}) = 1


第一章:SG函数的定义与计算

1.1 SG 函数的定义

对于公平组合游戏中的状态 xx,其 SG 函数值定义为:

SG(x)=mex({SG(y)∣y 是 x 的后继状态})SG(x)=mex({SG(y)∣y 是 x 的后继状态})

也就是说,一个状态的 SG 值等于其所有后继状态的 SG 值集合中未出现的最小非负整数

理解 SG 函数的一个关键视角:SG(x) = k 意味着从状态 x 可以转移到 SG 值为 0, 1, ..., k-1 的所有状态。这使得 SG 值与 Nim 游戏中的一堆石子形成了完美的对应关系。

1.2 计算 SG 函数的步骤

第一步:确定状态的表示

明确每个博弈状态如何表示。例如 Nim 游戏中,状态就是"各堆石子的数量"。

第二步:画出博弈图(有向无环图)

从终止状态开始,递推计算每个状态的 SG 值。

第三步:应用 mex 运算

对于每个状态,收集所有后继状态的 SG 值,取 mex。

第四步:利用 SG 定理组合

如果游戏由多个独立子游戏组成,总 SG 值为各子游戏 SG 值的异或和。

1.3 手算示例:简单的取石子游戏

游戏规则:一堆石子,每次可以取 1 颗或 3 颗,取到最后一颗的人赢。

计算 SG 值:

  • SG(0)=0(没有石子可取,必败)

  • SG(1)=mex({SG(0)})=mex({0})=1

  • SG(2)=mex({SG(1)})=mex({1})=0

  • SG(3)=mex({SG(2),SG(0)})=mex({0,0})=mex({0})=1

  • SG(4)=mex({SG(3),SG(1)})=mex({1,1})=0

规律:SG(x)=0SG(x)=0 当且仅当 xx 是偶数。

这个例子说明:SG 值为 0 的状态对应必败态 ,SG 值不为 0 的状态对应必胜态


第二章:SG定理与Nim游戏

2.1 SG 定理

SG 定理(Sprague-Grundy Theorem)是公平组合游戏理论的基石:

一个组合游戏(由若干子游戏组成)的 SG 值,等于各子游戏 SG 值的异或和(Nim 和)

换句话说,如果游戏 GG 可以分解为若干互不影响的子游戏 G1,G2,...,GnG1​,G2​,...,Gn​,那么:

SG(G)=SG(G1)⊕SG(G2)⊕⋯⊕SG(Gn)

胜负判定

  • 若 SG(G)=0,则先手必败

  • 若 SG(G)≠0,则先手必胜

这正是 Nim 游戏结论的推广:Nim 游戏中每堆石子就是一个子游戏,SG(一堆石子)=石子数SG(一堆石子)=石子数,所以总 SG 值就是所有堆石子数的异或和。

2.2 SG 定理的直观理解

为什么 SG 值可以异或?因为 SG 值 k 意味着该状态可以转移到 SG 值为 0, 1, ..., k-1 的任意状态。这和 Nim 游戏中一堆 k 颗石子的性质完全一样------你可以把它变成 0, 1, ..., k-1 颗。

因此,整个游戏等价于一个 Nim 游戏,其中第 ii 堆石子的数量就是第 ii 个子游戏的 SG 值。


第三章:性质与复杂度

性质 说明
SG = 0 必败 SG 值为 0 的状态是必败态
SG ≠ 0 必胜 SG 值不为 0 的状态是必胜态
SG 定理 总 SG = 各子游戏 SG 的异或和
计算方式 从终止状态开始递推,或 DFS + 记忆化
适用场景 所有公平组合游戏
复杂度 O(状态数×转移数)O(状态数×转移数)

第四章:例题与详细解析

例题1:Nim游戏 ------ 洛谷 P2197

题目描述

地上有 nn 堆石子,每堆石子数量为 aiai​。两人轮流操作,每次可以从任意一堆 中取出任意数量的石子(至少取 1 颗,可以取完)。不能操作者输。判断先手是否必胜。

输入示例

复制代码
1
3
1 2 3

输出示例

复制代码
Yes

解题思路

这是 Nim 游戏的模板题。每堆石子是一个独立的子游戏,SG(一堆 x 颗石子)=xSG(一堆 x 颗石子)=x。根据 SG 定理,总 SG 值为所有堆石子数的异或和。

详细解析

第一步:认识 Nim 游戏的结论

Nim 游戏有一个著名的定理:先手必胜当且仅当所有堆石子数的异或和不为 0

证明思路(简要):

  • 若异或和为 0,无论先手如何操作,异或和都会变为非 0,后手总能把异或和重新变为 0。

  • 若异或和非 0,先手总能找到一堆石子,从中取出若干颗,使异或和变为 0。

  • 最终所有堆石子数为 0 时,异或和为 0,轮到谁谁输。

第二步:代码实现

复制代码
#include <bits/stdc++.h>
using namespace std;

int main() {
    int T;
    cin >> T;
    while (T--) {
        int n;
        cin >> n;
        int xorsum = 0;
        for (int i = 0; i < n; i++) {
            int a;
            cin >> a;
            xorsum ^= a;
        }
        cout << (xorsum ? "Yes" : "No") << '\n';
    }
    return 0;
}

第三步:复杂度分析

时间复杂度 O(n)O(n),空间复杂度 O(1)O(1)。


例题2:高手过招 ------ 洛谷 P2575

题目描述

有一个 n×20n×20 的棋盘,每个格子有棋子(1)或没有棋子(0)。每次操作:选择一个棋子,将它向右移动到第一个空格处。如果某个棋子右边没有空格,则不能移动该棋子。当所有棋子都不能移动时,游戏结束,不能操作者输。判断先手是否必胜。

输入示例

复制代码
2
3
1 2 3
2 4 5

输出示例

复制代码
YES

解题思路

这道题不能直接套用 Nim 游戏的结论,需要先将其转化为阶梯 Nim(Staircase Nim) 问题。

详细解析

第一步:观察游戏性质

每一行是独立的子游戏,总 SG 值为各行 SG 值的异或和。

对于一行 20 个格子,从右往左看,连续的棋子构成"一段"。将相邻的有棋子的格子合并为一个"阶梯",阶梯上的棋子数等于该段连续棋子的长度。

第二步:转化为阶梯 Nim

阶梯 Nim 的结论是:只看奇数层阶梯上的石子数,异或和不为 0 则先手必胜

为什么?因为移动一个棋子到右边的空格,等价于把石子从当前阶梯移到更低的阶梯。这和阶梯 Nim 的规则完全一致。

第三步:代码实现

复制代码
#include <bits/stdc++.h>
using namespace std;

int main() {
    int T;
    cin >> T;
    while (T--) {
        int n;
        cin >> n;
        int ans = 0;
        
        for (int i = 0; i < n; i++) {
            int x;
            cin >> x;
            vector<int> pos(20, 0);
            for (int j = 0; j < x; j++) {
                int p;
                cin >> p;
                pos[p-1] = 1;  // 标记有棋子的位置
            }
            
            // 从右往左处理,转化为阶梯 Nim
            int cnt = 0, step = 0;
            for (int j = 19; j >= 0; j--) {
                if (pos[j] == 0) {
                    step++;  // 空格增加,进入下一级阶梯
                } else {
                    if (step % 2 == 1) cnt++;  // 奇数层阶梯上的棋子数
                }
            }
            ans ^= cnt;  // 每行的 SG 值异或起来[reference:76]
        }
        
        cout << (ans ? "YES" : "NO") << '\n';
    }
    return 0;
}

第四步:复杂度分析

每行 20 个格子,nn 行,时间复杂度 O(20n)O(20n),空间复杂度 O(1)O(1)。


第五章:常见变体与应用场景

变体 核心思路 典型例题
Nim 游戏 所有堆异或和 P2197
阶梯 Nim 只统计奇数层阶梯 P2575
Anti-Nim 取到最后一颗石子的人输 需特殊判断
拆分 Nim 可将一堆分成两堆,SG 打表找规律 HDU 3032
树上博弈 转化为阶梯 Nim 或 SG 函数 各类树上删边游戏
SG 打表找规律 小范围计算 SG,观察周期规律 HDU 5795

总结

SG 函数是解决公平组合游戏问题的统一框架。它的核心流程是:

  1. 确定状态:明确每个局面如何表示。

  2. 计算 SG 值 :从终止状态开始,利用 mex 运算递推。

  3. 应用 SG 定理:将总游戏的 SG 值分解为各子游戏 SG 值的异或和。

  4. 判断胜负:SG 值为 0 则必败,否则必胜。

从 P2197(Nim 游戏模板)的"直接异或"到 P2575(高手过招)的"转化为阶梯 Nim",SG 函数的价值在于化繁为简------无论游戏规则多么复杂,只要能分解为若干独立的子游戏,就能用 SG 定理统一求解