题解:AcWing 894 拆分-Nim游戏
【题目来源】
AcWing:894 拆分-Nim游戏 - AcWing题库
【题目描述】
给定 \(n\) 堆石子,两位玩家轮流操作,每次操作可以取走其中的一堆石子,然后放入两堆规模更小的石子(新堆规模可以为 \(0\),且两个新堆的石子总数可以大于取走的那堆石子数),最后无法进行操作的人视为失败。
问如果两人都采用最优策略,先手是否必胜。
【输入】
第一行包含整数 \(n\)。
第二行包含 \(n\) 个整数,其中第 \(i\) 个整数表示第 \(i\) 堆石子的数量 \(a_i\)。
【输出】
如果先手方必胜,则输出 Yes。
否则,输出 No。
【输入样例】
2
2 3
【输出样例】
Yes
【核心思想】
-
问题分析:给定 \(n\) 堆石子,每次操作可以取走一整堆石子,并替换为两堆规模更小的石子(新堆大小可以为 \(0\),两新堆之和可以大于原堆)。无法操作者失败。与标准 Nim(只能取走)和集合 Nim(只能减去固定数量)不同,本题的核心操作是"一堆变两堆",属于复合游戏拆分,必须用 Sprague-Grundy 定理分析。
-
算法选择:
- Sprague-Grundy(SG)定理:整个复合游戏的 SG 值等于各独立子游戏 SG 值的异或和。先手必胜当且仅当总异或和非零
- SG 函数递归定义:对于一堆 \(x\) 个石子,一次操作将其拆分为两堆 \(i\) 和 \(j\)(\(0 \leq j \leq i < x\)),产生两个独立的子游戏。因此后继状态的 SG 值为 \(\text{sg}(i) \oplus \text{sg}(j)\)
- mex 运算与记忆化:\(\text{sg}(x) = \text{mex}\{\text{sg}(i) \oplus \text{sg}(j) \mid 0 \leq j \leq i < x\}\),通过数组
f[x]记忆已计算值
-
关键步骤:
- 读取输入:石子堆数 \(n\) 和每堆石子数 \(a_1, \ldots, a_n\)
- 计算单堆 SG 函数值(
sg(x)):- 边界:若
f[x] != -1,直接返回记忆化值 - 枚举所有拆分方式:外层 \(i\) 从 \(0\) 到 \(x-1\)(第一堆大小),内层 \(j\) 从 \(0\) 到 \(i\)(第二堆大小,\(j \leq i\) 避免重复)
- 每个拆分产生两个独立子游戏,其后继 SG 值为 \(\text{sg}(i) \oplus \text{sg}(j)\),加入集合 \(S\)
- mex 运算:从 \(0\) 开始枚举,找到第一个不在 \(S\) 中的非负整数,记为 \(\text{sg}(x)\)
- 记忆化:
f[x] = sg(x)
- 边界:若
- 计算总异或和:
res ^= sg(a_i),遍历所有 \(n\) 堆石子 - 判断胜负:
res != 0则输出Yes(先手必胜),否则No
-
时间/空间复杂度:
- 时间复杂度:\(O(n \cdot H^2)\),其中 \(H = \max(a_i)\)。对于每个状态 \(x\),需要枚举约 \(x^2/2\) 种拆分方式,即 \(O(H^2)\),共计算 \(H\) 个状态。实际由于记忆化,每个状态只计算一次,单次
sg(x)为 \(O(x^2)\),总计 \(O(H^3)\)。但对于 \(H \leq 100\) 的数据范围完全可接受 - 空间复杂度:\(O(H)\),存储 SG 函数记忆化数组
f[0..H]
- 时间复杂度:\(O(n \cdot H^2)\),其中 \(H = \max(a_i)\)。对于每个状态 \(x\),需要枚举约 \(x^2/2\) 种拆分方式,即 \(O(H^2)\),共计算 \(H\) 个状态。实际由于记忆化,每个状态只计算一次,单次
-
拆分-Nim 与 SG 定理的核心思想:
- "一堆变两堆"的 SG 值计算:与"取走"或"减去"不同,拆分操作将原游戏替换为两个独立子游戏的组合。根据 SG 定理,组合状态的 SG 值等于两个子游戏 SG 值的异或,因此后继状态集合为 \(\{\text{sg}(i) \oplus \text{sg}(j)\}\)
- 无重复枚举技巧:内层循环 \(j\) 从 \(0\) 到 \(i\)(而非 \(x-1\)),避免 \((i, j)\) 与 \((j, i)\) 重复计算,减少约一半计算量
- SG 函数的递归结构:本题完美展示了 SG 函数的递归定义——状态的 SG 值仅依赖于规模更小的状态的 SG 值。这种"自底向上"的依赖关系保证了记忆化搜索的正确性和高效性
- 与标准 Nim 的区别:标准 Nim 中 \(\text{sg}(x) = x\),而拆分 Nim 中 \(\text{sg}(x)\) 与 \(x\) 无简单线性关系,必须通过递归计算。例如 \(\text{sg}(0) = 0\),\(\text{sg}(1) = \text{mex}\{\text{sg}(0)\oplus\text{sg}(0)\} = \text{mex}\{0\} = 1\),\(\text{sg}(2) = \text{mex}\{\text{sg}(0)\oplus\text{sg}(0), \text{sg}(1)\oplus\text{sg}(0), \text{sg}(1)\oplus\text{sg}(1)\} = \text{mex}\{0, 1, 0\} = 2\)
- 适用于:复合游戏拆分、一堆变多堆的博弈问题、需要递归计算 SG 函数的公平组合游戏
【算法标签】
博弈论
【代码详解】
#include <bits/stdc++.h>
#include <unordered_set>
using namespace std;
const int N = 105; // 最大石子数
int f[N]; // 记忆化SG函数值,f[x]表示有x个石子的SG值
// 计算有x个石子的SG函数值
int sg(int x)
{
// 如果已经计算过,直接返回记忆化的值
if (f[x] != -1)
{
return f[x];
}
// 存储所有可能分裂后状态的SG值的异或和
unordered_set<int> S;
// 遍历所有可能的拆分方式
// 将x个石子拆分成两堆:一堆i个,一堆j个
// 其中 i + j < x(至少剩下1个石子用于拆分)
for (int i = 0; i < x; i++) // 第一堆的大小,0 ≤ i < x
{
for (int j = 0; j <= i; j++) // 第二堆的大小,0 ≤ j ≤ i
{
// 拆分成两堆(i,j)后,形成两个独立的游戏
// 组合游戏的SG值是两个独立游戏SG值的异或
S.insert(sg(i) ^ sg(j));
}
}
// mex操作:找到最小的不在集合S中的非负整数
for (int i = 0; ; i++)
{
if (!S.count(i)) // 如果i不在集合S中
{
return f[x] = i; // 记忆化并返回SG值
}
}
}
int main()
{
int n; // 石子堆数
cin >> n;
// 初始化SG函数记忆化数组为-1(表示未计算)
memset(f, -1, sizeof(f));
int res = 0; // 所有石子堆SG值的异或和
// 输入每堆的石子数,并计算SG值的异或和
for (int i = 0; i < n; i++)
{
int x; // 当前堆的石子数
cin >> x;
res ^= sg(x); // 计算当前堆的SG值并异或
}
// 根据SG定理判断胜负:
// 如果所有石子堆SG值的异或和不为0,先手必胜
// 如果所有石子堆SG值的异或和为0,先手必败
if (res)
{
cout << "Yes" << endl; // 先手必胜
}
else
{
cout << "No" << endl; // 先手必败
}
return 0;
}
【运行结果】
2
2 3
Yes
浙公网安备 33010602011771号