题解:AcWing 893 集合-Nim游戏
【题目来源】
AcWing:893 集合-Nim游戏 - AcWing题库
【题目描述】
给定 \(n\) 堆石子以及一个由 \(k\) 个不同正整数构成的数字集合 \(S\)。
现在有两位玩家轮流操作,每次操作可以从任意一堆石子中拿取石子,每次拿取的石子数量必须包含于集合 \(S\),最后无法进行操作的人视为失败。
问如果两人都采用最优策略,先手是否必胜。
【输入】
第一行包含整数 \(k\),表示数字集合 \(S\) 中数字的个数。
第二行包含 \(k\) 个整数,其中第 \(i\) 个整数表示数字集合 \(S\) 中的第 \(i\) 个数 \(s_i\)。
第三行包含整数 \(n\)。
第四行包含 \(n\) 个整数,其中第 \(i\) 个整数表示第 \(i\) 堆石子的数量 \(h_i\)。
【输出】
如果先手方必胜,则输出 Yes。
否则,输出 No。
【输入样例】
2
2 5
3
2 4 7
【输出样例】
Yes
【核心思想】
-
问题分析:给定 \(n\) 堆石子,以及一个包含 \(k\) 个不同正整数的集合 \(S = \{s_1, s_2, \ldots, s_k\}\)。两位玩家轮流操作,每次只能从一堆中取走属于 \(S\) 中的数量的石子。无法操作者失败。与标准 Nim 游戏不同,本题每次可取的石子数受集合 \(S\) 限制,属于受限 Nim 游戏,需要用Sprague-Grundy(SG)定理来求解。
-
算法选择:
- Sprague-Grundy(SG)定理:任意 impartial game 都可以等价为一个 Nim 堆,其大小由 SG 函数值决定。整个复合游戏的 SG 值为各子游戏 SG 值的异或和。先手必胜当且仅当异或和不为 \(0\)
- SG 函数(Sprague-Grundy Function):对于状态 \(x\),\(\text{SG}(x) = \text{mex}\{\text{SG}(y) \mid y \text{ 是 } x \text{ 的后继状态}\}\),其中 \(\text{mex}\)(Minimum Excluded value)表示集合中未出现的最小非负整数
- 记忆化搜索(Memoization):由于 SG 函数值具有递归定义,使用数组
f[x]记忆已计算的状态,避免重复递归
-
关键步骤:
- 读取输入:集合大小 \(k\)、集合 \(S\)、石子堆数 \(n\)、每堆石子数 \(h_1, \ldots, h_n\)
- 计算单堆 SG 函数值(
sg(x)):- 边界:若
f[x] != -1,说明已计算过,直接返回 - 枚举所有可行动作:对于每个 \(s_i \in S\),若 \(x \geq s_i\),则后继状态为 \(x - s_i\),递归计算 \(\text{sg}(x - s_i)\) 并插入集合
- mex 运算:从 \(0\) 开始枚举,找到第一个未在集合中出现的非负整数,即为 \(\text{sg}(x)\)
- 记忆化:
f[x] = sg(x)
- 边界:若
- 计算总异或和:
- 初始化
res = 0 - 对每堆石子数 \(h_i\):\
res ^= sg(h_i)
- 初始化
- 判断胜负:
- 若
res != 0,输出Yes(先手必胜) - 若
res == 0,输出No(先手必败)
- 若
-
时间/空间复杂度:
- 时间复杂度:\(O(n \cdot H \cdot k)\),其中 \(H = \max(h_i)\) 为最大石子堆数。每堆石子需要计算 SG 值至 \(h_i\),每个状态枚举 \(k\) 种取法
- 空间复杂度:\(O(H)\),用于存储 SG 函数记忆化数组
f[0..H]
-
Sprague-Grundy 定理与 SG 函数的核心思想:
- SG 函数的本质:将任意复杂的 impartial game 状态映射为一个非负整数(nimber),该整数等价于一个标准 Nim 堆的大小。状态的 SG 值由其所有后继状态的 SG 值通过 mex 运算唯一确定
- mex 运算的含义:\(\text{mex}(S)\) 是集合 \(S\) 中未出现的最小非负整数。它保证了 SG 值能够区分"必胜态"和"必败态"——SG 值为 \(0\) 的状态对应 P-position(必败),非 \(0\) 对应 N-position(必胜)
- SG 定理的分解能力:对于由多个独立子游戏构成的复合游戏,整体 SG 值等于各子游戏 SG 值的异或和。这正是为什么本题可以分别计算每堆石子的 SG 值再异或的原因
- 记忆化搜索的必要性:SG 函数值具有重叠子问题性质(多个状态可能转移到同一个后继状态),记忆化将指数级复杂度降为多项式级
- 与标准 Nim 的联系:当 \(S = \{1, 2, \ldots, \infty\}\)(即可以取任意数量石子)时,\(\text{sg}(x) = x\),SG 定理退化为标准 Nim 游戏的异或和判定
- 适用于:多堆受限取石子游戏、各种可分解为独立子游戏的 impartial game 胜负判定
【算法标签】
博弈论
【代码详解】
#include <bits/stdc++.h>
using namespace std;
const int N = 110, M = 10005; // 定义常量 N 和 M
int n, k; // n 表示石子的堆数,k 表示每次可以取的石子数的种类数
int s[N]; // s 数组存储每次可以取的石子数
int f[M]; // f 数组用于记忆化搜索,存储每个状态的 SG 值
// 计算 SG 值
int sg(int x)
{
if (f[x] != -1) return f[x]; // 如果已经计算过,直接返回
set<int> S; // S 用于存储所有可能的 SG 值
for (int i = 0; i < k; i++) { // 遍历所有可能的取石子数
if (x >= s[i]) S.insert(sg(x - s[i])); // 递归计算 SG 值并插入 S
}
for (int i = 0; i <= x; i++) { // 找到最小的未出现在 S 中的非负整数
if (S.count(i) == 0) return f[x] = i; // 返回该整数作为 SG 值
}
}
int main()
{
cin >> k; // 输入每次可以取的石子数的种类数 k
for (int i = 0; i < k; i++) cin >> s[i]; // 输入每次可以取的石子数
cin >> n; // 输入石子的堆数 n
memset(f, -1, sizeof(f)); // 初始化 f 数组为 -1(表示未计算)
int ans = 0; // 初始化异或结果为 0
for (int i = 0; i < n; i++) { // 遍历每堆石子
int x; // 定义变量 x,表示当前堆的石子数
cin >> x; // 输入当前堆的石子数
ans ^= sg(x); // 计算当前堆的 SG 值并与异或结果进行异或
}
if (ans) puts("Yes"); // 如果异或结果不为 0,输出 Yes
else puts("No"); // 否则输出 No
return 0; // 程序结束
}
#include <bits/stdc++.h>
using namespace std;
const int N = 105; // 最大可取石子数集合大小
const int M = 10005; // 最大石子堆大小
int n, k; // n: 石子堆数, k: 可取石子数集合大小
int s[N]; // 可取石子数的集合
int f[M]; // 记忆化SG函数值,f[x]表示有x个石子的SG值
// 计算有x个石子的SG函数值
int sg(int x)
{
// 如果已经计算过,直接返回记忆化的值
if (f[x] != -1)
{
return f[x];
}
// 存储所有后继状态的SG值
unordered_set<int> S;
// 遍历所有可能的操作:从x个石子中取走s[i]个
for (int i = 0; i < k; i++)
{
int sum = s[i]; // 这次可以取走的石子数
if (x >= sum) // 如果当前石子数足够取走s[i]个
{
// 取走s[i]个石子后,剩下x-sum个石子
// 记录这个后继状态的SG值
S.insert(sg(x - sum));
}
}
// mex运算:找到最小的不在集合S中的非负整数
for (int i = 0; ; i++)
{
if (!S.count(i)) // 如果i不在集合S中
{
return f[x] = i; // 记忆化并返回SG值
}
}
}
int main()
{
// 输入可取石子数的集合
cin >> k;
for (int i = 0; i < k; i++)
{
cin >> s[i];
}
// 输入石子堆数
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 5
3
2 4 7
Yes
浙公网安备 33010602011771号