惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

D
Docker
G
Google Developers Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
GbyAI
GbyAI
V
Vulnerabilities – Threatpost
Hugging Face - Blog
Hugging Face - Blog
I
Intezer
S
Securelist
Forbes - Security
Forbes - Security
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Jina AI
Jina AI
Y
Y Combinator Blog
N
News | PayPal Newsroom
S
Schneier on Security
O
OpenAI News
T
The Blog of Author Tim Ferriss
V
Visual Studio Blog
Simon Willison's Weblog
Simon Willison's Weblog
Martin Fowler
Martin Fowler
人人都是产品经理
人人都是产品经理
雷峰网
雷峰网
NISL@THU
NISL@THU
阮一峰的网络日志
阮一峰的网络日志
WordPress大学
WordPress大学
N
News and Events Feed by Topic
Microsoft Azure Blog
Microsoft Azure Blog
P
Proofpoint News Feed
The Cloudflare Blog
Last Week in AI
Last Week in AI
博客园 - 司徒正美
L
LangChain Blog
C
CERT Recently Published Vulnerability Notes
L
LINUX DO - 热门话题
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
aimingoo的专栏
aimingoo的专栏
Apple Machine Learning Research
Apple Machine Learning Research
Recent Commits to openclaw:main
Recent Commits to openclaw:main
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
The Hacker News
The Hacker News
博客园 - Franky
Attack and Defense Labs
Attack and Defense Labs
Security Latest
Security Latest
T
Tailwind CSS Blog
博客园_首页
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Microsoft Security Blog
Microsoft Security Blog
V2EX - 技术
V2EX - 技术
腾讯CDC
V
V2EX

博客园 - RonChen

区间合并 距离和的最小值 归并排序与逆序对 快速排序与快速选择 同余分析 差分约束 Treap 点分治 莫队算法 分块 扫描线 错排问题 容斥原理 卢卡斯定理 线性基 高斯消元 勒让德公式 次短路 分层图最短路 01 图最短路 洪水填充 双向搜索 迭代加深搜索 剪枝 最小表示法 表达式计算 KMP 算法 队列 高精度运算
Sprague-Grundy (SG) 函数及其应用
RonChen · 2026-04-03 · via 博客园 - RonChen

在算法竞赛中,博弈论问题通常涉及到两名玩家轮流操作。要理解复杂的博弈模型,通常从最基础、最经典的 Nim 游戏 开始。

Nim 游戏

Nim 游戏是博弈论中最基础的模型。

游戏规则

\(n\) 堆石子,第 \(i\) 堆石子数为 \(a_i\)。两名玩家轮流从任意一堆中取走任意数量的石子(至少取一个,至多取完该堆),最后取光石子的人获胜。

Nim 和

Nim 游戏的胜负由所有堆石子数的异或和(称为 Nim 和)决定:\(S = a_1 \oplus a_2 \oplus \cdots \oplus a_n\)

  • \(S \ne 0\),当前状态为必胜态,先手总能通过一次操作将 \(S\) 变为 0。
  • \(S = 0\),当前状态为必败态。无论先手如何操作,产生的 \(S'\) 必定不为 0。

证明

  1. 终局状态:当所有 \(a_i = 0\) 时,游戏结束,此时 \(S = 0 \oplus 0 \oplus \cdots \oplus 0 = 0\),这符合必败态的定义。
  2. \(S \neq 0 \Rightarrow S = 0\) 的转移:设 \(S\) 的二进制表示中最高位的 1 在第 \(k\) 位,那么至少存在一堆石子 \(a_i\),其第 \(k\) 位也是 1。显然 \(a_i \oplus S \lt a_i\),因为异或 \(S\) 之后,\(a_i\) 的第 \(k\) 位变成了 0,而更高的位没变。将第 \(i\) 堆石子数从 \(a_i\) 减少到 \(a_i' = a_i \oplus S\),新的异或和为 \(S' = S \oplus a_i \oplus (a_i \oplus S) = S \oplus S = 0\)
  3. \(S = 0 \Rightarrow S \neq 0\) 的转移:若从第 \(i\) 堆取走一些石子,使其从 \(a_i\) 变为 \(a_i' \ (a_i' \lt a_i)\)。新的异或和 \(S' = S \oplus a_i \oplus a_i' = 0 \oplus a_i \oplus a_i' = a_i \oplus a_i'\),由于 \(a_i \ne a_i'\),故 \(S' \ne 0\)

例题:P2197 【模板】Nim 游戏

参考代码
#include <cstdio>
int main()
{
    int t; scanf("%d", &t);
    while (t--) {
        int n; scanf("%d", &n);
        int res = 0;
        for (int i = 0; i < n; i++) {
            int x; scanf("%d", &x);
            res ^= x;
        }
        if (res != 0) printf("Yes\n");
        else printf("No\n");
    }
    return 0;
}

习题:P4301 [CQOI2013] 新Nim游戏

本题是传统 Nim 游戏的变种,设有 \(k \ (1 \le k \le 100)\) 堆火柴,每堆火柴的数量分别为 \(a_i \ (1 \le a_i \le 10^9)\),规则如下:

  1. 第一回合:先手(A)可以拿走若干整堆火柴,接着后手(B)也可以拿走若干整堆火柴。注意:第一回合双方拿走的都是“整堆”,且不能把火柴全部拿完。
  2. 后续回合:从先手(A)开始,按照传统 Nim 游戏规则进行(每次从一堆中拿走至少一根火柴,拿走最后一根的人获胜)。

目标:作为先手,求在保证必胜的情况下,第一回合拿走的火柴数量的最小值。

解题思路

在传统 Nim 游戏中,若各堆火柴数量的异或和不为 0,则先手必胜。

在本题中,第一回合结束后,游戏进入传统 Nim 阶段,此时轮到先手(A)操作。如果 A 想要必胜,必须保证:无论后手(B)在第一回合如何拿走整堆火柴,剩下堆的异或和始终不能为 0

如果 A 留下的火柴堆集合是线性无关的,那么 B 无论拿走哪些堆,剩下的子集异或和永远不会为 0。为了让 A 第一回合拿走的火柴最少,等价于让 A 留下的火柴总数最多。

这个问题类似于 P4570 [BJWC2011] 元素,具有贪心性质,将所有火柴堆按数量从大到小排序,依次尝试将每一堆火柴加入线性基:如果当前堆能够成功插入线性基(即它不能被已有的堆异或表示出来),说明它与已有的堆线性无关,可以保留;如果无法插入,说明这一对必须在第一回合被拿走。

参考代码
#include <cstdio>
#include <algorithm>
#include <functional>
using namespace std;
using ll = long long;
const int N = 105;
int a[N], d[30];
// 线性基插入函数
bool insert(int x) {
    for (int i = 29; i >= 0; i--) {
        if (!((x >> i) & 1)) continue;
        if (!d[i]) {
            d[i] = x;
            return true;
        }
        x ^= d[i];
    }
    return false;
}
int main()
{
    int k; scanf("%d", &k);
    ll sum = 0;
    for (int i = 0; i < k; i++) {
        scanf("%d", &a[i]);
        sum += a[i];
    }
    // 贪心策略:按火柴数从大到小排序,尽量保留大的火柴堆
    sort(a, a + k, greater<int>());
    ll keep = 0;
    for (int i = 0; i < k; i++) {
        // 如果当前堆与已保留的堆线性无关,则将其保留
        if (insert(a[i])) keep += a[i];
    }
    // 结果为总数减去保留的数量
    printf("%lld\n", sum - keep);
    return 0;
}

变体:阶梯 Nim 博弈

游戏规则

有一个 \(n\) 层的阶梯,每层阶梯上放着若干个硬币。玩家轮流操作,每次可以从第 \(i\) 层阶梯(\(i \ge 1\))中取走任意数量的硬币,并将它们放到第 \(i-1\) 层,而降落到第 0 层(地面)的硬币就不能再移动了。最后一个移动硬币的玩家获胜,即无法移动者输。

结论与博弈策略

阶梯 Nim 博弈的核心结论是这个游戏等价于只对“奇数层”阶梯上的硬币做经典 Nim 游戏

设第 \(i\) 层解题的硬币数量为 \(a_i\),当且仅当所有奇数层硬币数量的异或和为 0 时,当前玩家处于必败态。而只要上述异或和不为 0,当前玩家就有必胜策略。

为什么偶数层不影响胜负?

困惑的地方在于既然可以移动偶数层的硬币,为什么它们不计入异或和?

这其实是一种“对抗策略”:如果对手移动奇数层,这就变成了经典 Nim,只需要根据 Nim 的策略调整另一个奇数层(或该层),使奇数层的异或和重新变回 0;如果对手移动偶数层,例如从第 2 层移到第 1 层,此时奇数层(第 1 层)的硬币增加了,作为应对,只需要立即将对手刚才移到第 1 层的那些硬币,全部再移到第 0 层,那么奇数层(第 1 层)的硬币数恢复原样,异或和不变,由于硬币最终都会掉进第 0 层(终点),这种“搬运”不可能无限进行,所以移动偶数层永远无法改变奇数层的博弈局面。

习题:P10507 Georgia and Bob

在无限长的棋盘上有 \(n \ (n \le 1000)\) 个棋子,两名玩家轮流将一枚棋子向左移动至少一格,但不能逾越其他棋子,也不能重合,无法操作的玩家输。给定棋子的初始位置 \(P_i \ (P_i \le 10000)\),判断先手是否必胜。

解题思路

首先将棋子位置从小到大排序得到 \(P_1 \lt P_2 \lt \cdots \lt P_n\),考虑相邻棋子之间的“空格”数量。由于棋子不能互相逾越,棋子的移动实际上是在改变这些空格的大小。

将空格定义为 \(g_1 = P_n - P_{n-1} - 1\)(最右边两个棋子之间的空格),\(g_2 = P_{n-1} - P_{n-2} - 1\),……,\(g_n = P_1 - 1\)(最左边的棋子与位置 0 之间的空格)。

当移动第 \(n\) 枚棋子时,\(g_1\) 减少。当移动第 \(n-1\) 枚棋子时,\(g_2\) 减少,同时 \(g_1\) 增加。当移动第 \(i\) 枚棋子时,\(g_{n-i+1}\) 减少,同时 \(g_{n-i}\) 增加。

这与阶梯 Nim 博弈的规则完全一致:有若干级阶梯,每级阶梯上有若干物品,每次可以将第 \(i\) 级阶梯的物品移动到第 \(i-1\) 级阶梯(第 1 级移动到地面)。

阶梯 Nim 博弈的关键结论是只有奇数级阶梯上的物品对胜负有影响:如果先手移动偶数级阶梯的物品到奇数级,后手可以立即将这些物品移动到下一级偶数级(或地面),使得奇数级阶梯的状态恢复原状。因此,阶梯 Nim 博弈等价于对所有奇数级阶梯上的物品数量进行普通的 Nim 博弈。

在本题中,从右往左数,第 1 个间距 \(g_1\) 是奇数级,第 2 个间距 \(g_2\) 是偶数级,第 3 个间距 \(g_3\) 是奇数级,……。只需要计算 \(g_1 \oplus g_3 \oplus g_5 \oplus \cdots\) 的结果,如果异或和不为 0,则先手必胜。

参考代码
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 1005;
int p[N], g[N];
void solve() {
    int n; scanf("%d", &n);
    for (int i = 0; i < n; i++) scanf("%d", &p[i]);
    sort(p, p + n);
    // 计算间距(从右往左)
    for (int i = n - 1; i > 0; i--) {
        g[n - i - 1] = p[i] - p[i - 1] - 1;
    }
    g[n - 1] = p[0] - 1;
    // 对奇数位间距求异或和 (索引 0, 2, 4...)
    int sum = 0;
    for (int i = 0; i < n; i += 2) {
        sum ^= g[i];
    }
    if (sum > 0) printf("Georgia will win\n");
    else printf("Bob will win\n");
}
int main()
{
    int t; scanf("%d", &t);
    while (t--) solve();
    return 0;
}

公平组合游戏

Nim 游戏属于一类被称为 公平组合游戏(Impartial Combinatorial Games) 的博弈,其特征如下:

  1. 有两名玩家,轮流进行操作。
  2. 在游戏的任意时刻,合法操作集合仅取决于当前的状态,而与当前轮到哪位玩家无关。
  3. 游戏在有限步内结束,且不存在平局。
  4. 遵循常规操作原则:最后一名进行合法操作的玩家获胜(即无法移动者输)。

mex 函数与 SG 函数

mex 函数(Minimum Excluded value)

\(\text{mex}(S)\) 定义为集合 \(S\) 中未出现的最小非负整数,如 \(\text{mex}(\{0, 1, 2, 4\}) = 3, \ \text{mex}(\{1, 3, 5\}) = 0\)

SG 函数

对于一个状态 \(u\),设其后继状态集合为 \(V = \{v_1, v_2, \dots, v_k\}\),则状态 \(u\) 的 SG 值定义为 \(\text{SG}(u) = \text{mex}(\{\text{SG}(v_1), \text{SG}(v_2), \dots, \text{SG}(v_k)\})\)

\(\text{SG}(u) = 0\) 表示该状态为必败态\(\text{SG}(u) \ne 0\) 表示该状态为必胜态

  • 对于没有任何后继移动的状态(游戏结束),\(\text{SG}(u) = 0 = \text{mex}(\emptyset) = 0\),对应必败态。
  • \(\text{SG}(u)=0\),根据 \(\text{mex}\) 的定义,这意味着 \(u\) 的所有后继状态 \(v_i\) 的 SG 值都不为 0(即 \(\text{SG}(v_i) \gt 0\)),这符合“必败态的任何移动都会进入必胜态”的性质。
  • \(\text{SG} \gt 0\),根据 \(\text{mex}\) 的定义,这意味着 \(u\) 的后继状态中至少存在一个状态 \(v_j\) 满足 \(\text{SG}(v_j) = 0\),这符合“必胜态至少有一种移动可以进入必败态”的性质。

Sprague-Grundy 定理

SG 定理是博弈论中最核心的定理,它指出任何一个 ICG 都可以等价于一个 Nim 游戏中的一堆石子,其石子数为该状态的 SG 值

对于由 \(n\) 个独立的子游戏组成的复合游戏 \(G\),其总状态的 SG 值为各子游戏状态 SG 值的异或和:\(\text{SG}(G) = \text{SG}(G_1) \oplus \text{SG}(G_2) \oplus \cdots \oplus \text{SG}(G_n)\)

要证明复合游戏 \(G\) 的 SG 值(设为 \(x\))确实等于各子游戏 SG 值的异或和。根据 SG 函数的定义,需要证明:

  1. 对于任何 \(x' \lt x\),都存在一个后继状态 \(G'\) 使得 \(\text{SG}(G') = x'\)
  2. 对于任何后继状态 \(G'\),都有 \(\text{SG}(G') \ne x\)

\(x = \text{SG}(G_1) \oplus \text{SG}(G_2) \oplus \cdots \oplus \text{SG}(G_n)\)

证明 1(可达性):设 \(k = x \oplus x'\),由于 \(x' \lt x\),在 \(x \oplus x'\) 的二进制表示中,最高位的 1(设在第 \(p\) 位)在 \(x\) 中必然是 1,而在 \(x'\) 中必然是 0。既然 \(x\) 的第 \(p\) 位是 1,根据异或性质,必然存在至少一个子游戏 \(G_i\),其中 \(\text{SG}(G_i)\) 的第 \(p\) 位是 1,那么 \(\text{SG}(G_i) \oplus k \lt \text{SG}(G_i)\)。根据 SG 函数的定义,子游戏 \(G_i\) 一定存在一个后继状态 \(G_i'\) 使得 \(\text{SG}(G_i') = \text{SG}(G_i) \oplus k\)(因为 \(\text{SG}(G_i)\) 是其后继 SG 值的 \(\text{mex}\),所以小于它的值一定能达到)。此时整个复合游戏的异或和变为 \(\text{SG}(G') = x \oplus \text{SG}(G_i) \oplus \text{SG}(G_i') = x \oplus k = x \oplus (x \oplus x') = x'\),这证明了可以到达任何小于 \(x\) 的 SG 值。

证明 2(不可达性):后继状态 \(G'\) 是通过移动其中一个子游戏 \(G_i\)\(G_i'\) 得到的,若 \(\text{SG}(G') = x\),即 \(\text{SG}(G_1) \oplus \dots \oplus \text{SG}(G_i') \oplus \dots \oplus \text{SG}(G_n) = \text{SG}(G_1) \oplus \dots \oplus \text{SG}(G_i) \oplus \dots \oplus \text{SG}(G_n)\)。等式两边同时异或掉相同的项,得到 \(\text{SG}(G_i') = \text{SG}(G_i)\)。这与 \(\text{SG}(G_i)\) 是其后继状态 SG 值的 \(\text{mex}\) 这一基础定义矛盾,因此 \(\text{SG}(G') \neq x\) 成立。

综上所述,\(\text{SG}(G)\) 完美符合 \(\text{mex}\) 的定义,定理得证。

例题:P10501 Cutting Game

给定一个 \(W \times H \ (2 \le W,H \le 200)\) 的矩形纸张,两名玩家轮流对纸张进行横向或纵向切割,每次切割将一片矩形纸张分成两片新的矩形。如果一名玩家切出了一个 \(1 \times 1\) 的格子,该玩家获胜。问在双方都采取最优决策略的情况下,先手是否必胜。

本题的关键在于获胜条件:切出 \(1 \times 1\) 格子的玩家获胜。观察发现,如果一个玩家在某一步切出了一个维度为 1 的矩形(例如 \(1 \times k\)\(k \times 1\)),那么下一个玩家可以立即从该矩形中切出一个 \(1 \times 1\) 的格子从而直接获胜。因此,在最优策略下,任何玩家都不会主动切出维度为 1 的矩形,除非当前矩形无法再进行“安全”的切割(即切出的两个部分边长都 \(\ge 2\))。此时,游戏等价于在一个“安全移动”集合上的博弈,当一名玩家无法再将矩形切成两个边长均 \(\ge 2\) 的小矩形时,他将不得不切出一个维度为 1 的矩形,从而输掉游戏。

该游戏是一个典型的公平组合游戏,可以使用 SG 定理求解。定义状态 \(\text{SG}(w,h)\) 表示宽度为 \(w\)、高度为 \(h\) 的矩形的博弈状态值。切割一个矩形会将原游戏分解为两个独立的子游戏,根据 SG 定理,一个移动的后继状态的 SG 值为两个子状态 SG 值的异或和。

\[\text{SG}(w, h) = \text{mex}(\{\text{SG}(k, h) \oplus \text{SG}(w-k, h) \mid 2 \le k \le w-2\} \cup \{\text{SG}(w, k) \oplus \text{SG}(w, h-k) \mid 2 \le k \le h-2\}) \]

对于 \(w \lt 4\)\(h \lt 4\) 的矩形,如果它们无法被切割成两个边长均 \(\ge 2\) 的矩形,则其 \(SG\) 值为 0。

由于 \(W,H \le 200\),可以预处理出所有可能的 \(\text{SG}(w,h)\) 值。对于每个状态,遍历所有可能的垂直切割位置和水平切割位置,记录出现的异或和,取最小未出现的非负整数。预处理完成后,对于每个测试用例,直接查询 \(\text{SG}(W,H)\),若其值大于 0,则先手必胜,否则先手必败。

时间复杂度为 \(O(W \times H \times (W+H))\)

参考代码
#include <cstdio>
int sg[205][205];
bool vis[512]; // 标记后继状态的 SG 异或和
// 预计算 200*200 范围内所有矩形的 SG 值
void init() {
    for (int i = 2; i <= 200; i++) {
        for (int j = 2; j <= 200; j++) {
            for (int k = 0; k < 512; k++) vis[k] = false;
            // 垂直切割:将宽度 i 切成 k 和 i-k,保证两个新矩形的宽度都 >= 2
            for (int k = 2; k <= i - k; k++) {
                vis[sg[k][j] ^ sg[i - k][j]] = true;
            }
            // 水平切割:将高度 j 切成 k 和 j-k,保证两个新矩形的高度都 >= 2
            for (int k = 2; k <= j - k; k++) {
                vis[sg[i][k] ^ sg[i][j - k]] = true;
            }
            // 计算 mex (最小未出现的非负整数)
            int k = 0;
            while (vis[k]) k++;
            sg[i][j] = k;
        }
    }
}
int main()
{
    init(); // 预处理 SG 表
    int w, h;
    while (scanf("%d%d", &w, &h) == 2) {
        // SG 值大于 0 表示先手必胜,否则必败
        if (sg[w][h] > 0) printf("WIN\n");
        else printf("LOSE\n");
    }
    return 0;
}

习题:P10506 魔法珠

\(n \ (1 \le n \le 100)\) 堆魔法珠,每一堆的数量分别为 \(a_i \ (1 \le a_i \le 1000)\),两人轮流进行操作:选择一堆数量 \(p \gt 1\) 的魔法珠;找到 \(p\) 的所有小于 \(p\) 的约数 \(b_1, b_2 \dots, b_m\);将该堆珠子替换为这 \(m\) 堆,每堆数量分别为 \(b_1, \dots, b_m\);从这 \(m\) 堆中选择一堆并将其移除。
游戏结束的条件是所有堆的数量都为 1(无法再进行操作),采用最佳策略,求先手还是后手获胜。

解题思路

这是一个典型的公平组合游戏,可以通过 SG 定理 解决。

根据 SG 定理,由多个独立子游戏组成的组合游戏,其总状态的 SG 值等于各子游戏状态 SG 值的异或和。在本题中,每堆魔法珠就是一个独立的子游戏。若 \(\bigoplus\limits_{i=1}^n \text{SG}(a_i) \ne 0\),则先手必胜。

对于一堆数量为 \(p\) 的珠子,设其小于 \(p\) 的约数集合为 \(\{b_1, b_2, \dots, b_m\}\),则操作过程为 \(p \to \{b_1, b_2, \dots, b_m\} \to \{b_1, \dots, b_{j-1}, b_{j+1}, \dots, b_m\}\)(移除了第 \(j\) 堆)。

一个后继状态的 SG 值等于剩余各堆 SG 值的异或和 \(\text{SG}(b_1) \oplus \cdots \oplus \text{SG}(b_{j-1}) \oplus \text{SG}(b_{j+1}) \oplus \cdots \oplus \text{SG}(b_m)\),相当于 \(\text{SG}(b_j) \oplus \bigoplus\limits_{i=1}^m \text{SG}(b_i)\)

根据 SG 函数的定义,\(\text{SG}(p) = \text{mex} \left( \left\{ \text{SG}(b_j) \oplus \bigoplus\limits_{i=1}^m \text{SG}(b_i) \mid j \in \{ 1, \dots, m \} \right\} \right)\)

参考代码
#include <cstdio>
int d[1005], sg[1005];
bool vis[1025];
// 预处理 1 到 1000 的 SG 值
void init() {
    for (int i = 2; i <= 1000; i++) {
        int cnt = 0, tot = 0;
        // 寻找 i 的所有真约数
        for (int j = 1; j < i; j++) {
            if (i % j == 0) {
                d[cnt] = j;
                cnt++;
                tot ^= sg[j];
            }
        }
        // 计算所有可能的后继状态的 SG 值
        for (int j = 0; j < 1025; j++) vis[j] = false;
        for (int j = 0; j < cnt; j++) vis[tot ^ sg[d[j]]] = true;
        // 求 mex
        int r = 0;
        while (vis[r]) r++;
        sg[i] = r;
    }
}
int main()
{
    init();
    int n; 
    while (scanf("%d", &n) == 1) {
        int sum = 0;
        for (int i = 0; i < n; i++) {
            int a; scanf("%d", &a);
            sum ^= sg[a];
        }
        // 若异或和不为 0,先手 Freda 必胜
        if (sum > 0) printf("freda\n");
        else printf("rainbow\n");
    }
    return 0;
}