题解: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

【核心思想】

  1. 问题分析:给定 \(n\) 堆石子,以及一个包含 \(k\) 个不同正整数的集合 \(S = \{s_1, s_2, \ldots, s_k\}\)。两位玩家轮流操作,每次只能从一堆中取走属于 \(S\) 中的数量的石子。无法操作者失败。与标准 Nim 游戏不同,本题每次可取的石子数受集合 \(S\) 限制,属于受限 Nim 游戏,需要用Sprague-Grundy(SG)定理来求解。

  2. 算法选择

    • 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] 记忆已计算的状态,避免重复递归
  3. 关键步骤

    • 读取输入:集合大小 \(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(先手必败)
  4. 时间/空间复杂度

    • 时间复杂度:\(O(n \cdot H \cdot k)\),其中 \(H = \max(h_i)\) 为最大石子堆数。每堆石子需要计算 SG 值至 \(h_i\),每个状态枚举 \(k\) 种取法
    • 空间复杂度:\(O(H)\),用于存储 SG 函数记忆化数组 f[0..H]
  5. 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
posted @ 2026-06-23 11:25  团爸讲算法  阅读(10)  评论(0)    收藏  举报