NeFut Logo NeFut
EN 管理员登录

深入解析 SG 函数与组合博弈论中的 mex 运算

发布于:2026-05-27 09:17 最后更新:2026-06-06 13:04
#algorithm #Game Theory #Combinatorics

核心逻辑与数学原理

SG 函数(Sprague-Grundy Function)是组合博弈论的终极武器。它将所有公平组合游戏(ICG)抽象并映射为有向无环图(DAG)上的数值,从而将任何复杂的博弈规则转化为经典 Nim 游戏的异或运算。

1. mex 运算(Minimum Excludant)

mex 运算是一个针对非负整数集合的特殊代数算子。定义 $ ext{mex}(S)$ 为不属于集合 $S$ 的最小非负整数

$$ ext{mex}(S) = ext{min} ig{ x ext{ in } ext{N} ig| x otin S ig\} $$

2. SG 函数的定义

对于游戏中的任意一个确定局势(状态)$x$,设它通过一步合法移动可以到达的后继状态集合为 $ ext{Successors}(x) = \\{y_1, y_2, \\dots, y_k\\ ext{)}$。则该状态的 SG 函数值定义为:

$$ ext{SG}(x) = ext{mex}ig( \{ ext{SG}(y) ig| y ext{ in } ext{Successors}(x) \} ig) $$

数学特征与 P/N 状态的映射

SG 函数值具有极强且精妙的代数物理特征:

其内部逻辑完美收敛于博弈三大公理:

  1. 终止态:若 $x$ 为无法移动的终止态,其后继集合为空集 $ ext{emptyset}$,则 $ ext{SG}(x) = ext{mex}( ext{emptyset}) = 0$(必败)。
  2. 由 0 迈向非 0:若 $ ext{SG}(x) = 0$,则其后继集合中绝对不可能包含 $0$。意味着从必败态出发,无论怎么走,后继状态的 SG 值必然大于 $0$(后继皆为必胜态)。
  3. 由非 0 迈向 0:若 $ ext{SG}(x) = k > 0$,根据 mex 的定义,在它的后继集合中,必然存在至少一个后继状态 $y$,满足 $ ext{SG}(y) = 0$。意味着从必胜态出发,总能一步剥落优势,把必败态丢给对手。

3. SG 定理(Sprague-Grundy Theorem)

如果一个游戏由 $n$ 个相互独立且互不干扰的子游戏复合而成(例如:游戏中有 $n$ 堆石子,每次只能选择其中一堆操作,各堆规则相同但数量独立),那么整个复合游戏的全局 SG 状态值,直接等于所有子游戏状态 SG 值的按位异或和

$$ ext{SG}_{ ext{global}} = ext{SG}(x_1) igoplus ext{SG}(x_2) igoplus \dots igoplus ext{SG}(x_n) $$

这就是为什么学界称“一切公平组合游戏本质上都是 Nim 游戏”。


算法推导与状态设计

1. 独立单堆局势的 SG 线性打表

对于常规单堆且动作受限的游戏(如:每次只能取走集合 $S=\\{1, 3, 4\ ext{)}$ 中的石子数),我们使用一维数组 sg[i] 记录石子数为 i 时的 SG 值。

利用桶排序(Vis 标记数组)实现高效的 mex 运算:

$$ ext{sg}[i] = ext{mex}ig(\{ ext{sg}[i - k] ig| k ext{ in } S ext{ and } i ext{ >= } k \} ig) $$

2. 游戏复合的异或解耦

当题目给出多堆石子时,由于子游戏完全独立,我们绝对不需要去开多维数组维护全局状态。只需要通过 $O(N)$ 线性算出单堆最大范围内的 sg 数组,接着对每堆石子的初始数量 $a_i$ 直接查表,将查出来的 sg[a_i] 进行全员异或。最终判断异或和是否为 $0$ 即可。


C++ 标准源码

#include <iostream>
#include <vector>
#include <cstring>
#include <algorithm>

const int MAXN = 10000;
int sg[MAXN + 5];

// 预处理单堆石子在特定规则(f 集合)下的 SG 值
void precompute_sg(const std::vector<int>& f, int max_stone) {
    std::memset(sg, 0, sizeof(sg)); // 隐式包含边界:sg[0] = 0

    // 临时标记数组,用于模拟 mex 运算的集合判定
    std::vector<bool> vis(max_stone + 5, false);

    for (int i = 1; i <= max_stone; ++i) {
        // 重置打标桶
        std::fill(vis.begin(), vis.end(), false);

        // 扫描所有合法的移动策略
        for (int move : f) {
            if (i >= move) {
                vis[sg[i - move]] = true; // 标记后继状态的 SG 值已出现
            }
        }

        // 执行 mex 运算:寻找最小的未被标记的非负整数
        int m = 0;
        while (vis[m]) {
            m++;
        }
        sg[i] = m; // 状态锁定
    }
}

int main() {
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(NULL);

    int k; // 移动限制集合的大小
    if (std::cin >> k) {
        std::vector<int> f(k);
        for (int i = 0; i < k; ++i) {
            std::cin >> f[i];
        }

        // 预处理打表,假设题目石子最大规模为 10000
        precompute_sg(f, MAXN);

        int m; // 询问的游戏局势组数
        if (std::cin >> m) {
            while (m--) {
                int n; // 当前这局游戏的石子堆数
                std::cin >> n;
                int global_sg_sum = 0;

                for (int i = 0; i < n; ++i) {
                    int stones;
                    std::cin >> stones;
                    global_sg_sum ^= sg[stones]; // 核心:多子游戏 SG 值强行异或
                }

                if (global_sg_sum != 0) {
                    std::cout << "W"; // 先手必胜
                } else {
                    std::cout << "L"; // 先手必败
                }
            }
            std::cout << "\n";
        }
    }
    return 0;
}

NOIP 实战避坑指南

  1. mex 运算中 vis 数组未逐层清空或清空范围错误 在计算 sg[i] 的内层循环前,必须将 vis 标记数组重置。有些选手贪图省事把 vis 开成全局的,每次只清空到 f 集合的大小,这会导致上一层状态的残余标记污染当前 mex 的判定,从而算出一堆完全偏离正确 mex 轨迹的数值。
  2. 错误地将“不独立”的游戏强行执行 SG 异或 使用 SG 定理的大前提是:各个子游戏相互独立,且每次操作只能干涉其中一个子游戏。如果题目魔改规则为“每次可以同时从两堆石子中取走相同数量”,此时这两堆石子之间产生了强烈的因果耦合,子游戏不再独立。这种情况下直接异或 sg[u] ^ sg[v] 会导致逻辑彻底崩溃。

经典 NOIP/洛谷 真题

1. 洛谷 P2189 小 Z 的游戏 / 类似 洛谷 P5663 [CSP-J2019] 加工零件 拓扑演化

$$ ext{sg}[u] = ext{mex}ig({ ext{sg}[v] ig| (u,v) ext{ in } E } \big) $$

所有节点 SG 值预处理完成后,读入每个棋子所在的初始节点编号,将这些节点的 SG 值全部异或起来。若异或和非 $0$ 则先手胜,否则后手胜。

2. 洛谷 P2147 [SDOI2009] E&D (深入变式)

$$ ext{SG}(x,y) = ext{mex}igg(ig{ ext{SG}(x_1, y_1) igoplus ext{SG}(x_2, y_2) ig} igg) $$

这展示了 SG 定理不仅可以用于最后求总和,更能直接嵌入到状态分裂的 mex 递推树中。

原文链接: local://22.3

[h] 返回首页