题解:洛谷 P1247 取火柴游戏

【题目来源】

洛谷:P1247 取火柴游戏 - 洛谷(luogu.com.cn)

【题目描述】

输入 \(k\)\(k\) 个整数 \(n_1,n_2,⋯ ,n_k\),表示有 \(k\) 堆火柴棒,第 \(i\) 堆火柴棒的根数为 \(n_i\);接着便是你和计算机取火柴棒的对弈游戏。取的规则如下:每次可以从一堆中取走若干根火柴,也可以一堆全部取走,但不允许跨堆取,也不允许不取。

谁取走最后一根火柴为胜利者。

例如:\(k=2\)\(n_1=n_2=2\),A 代表你,P 代表计算机,若决定 A 先取:

  • A:\((2,2)→(1,2)\),即从第一堆中取一根。
  • P:\((1,2)→(1,1)\),即从第二堆中取一根。
  • A:\((1,1)→(1,0)\)
  • P:\((1,0)→(0,0)\)。P 胜利。

如果决定 \(A\) 后取:

  • P:\((2,2)→(2,0)\)
  • A:\((2,0)→(0,0)\)。A 胜利。

又如 \(k=3\)\(n_1=1\)\(n_2=2\)\(n_3=3\)\(A\) 决定后取:

  • P:\((1,2,3)→(0,2,3)\)
  • A:\((0,2,3)→(0,2,2)\)
  • A 已将游戏归结为 \((2,2)\) 的情况,不管 P 如何取 A 都必胜。

编一个程序,在给出初始状态之后,判断是先取必胜还是先取必败,如果是先取必胜,请输出第一次该如何取。如果是先取必败,则输出 lose

【输入】

第一行,一个正整数 \(k\)

第二行,\(k\) 个整数 \(n_1,n_2,⋯ ,n_k\)

【输出】

如果是先取必胜,请在第一行输出两个整数 \(a,b\),表示第一次从第 \(b\) 堆取出 \(a\) 个。第二行为第一次取火柴后的状态。如果有多种答案,则输出 \(⟨b,a⟩\) 字典序最小的答案(即 \(b\) 最小的前提下,使 \(a\) 最小)。

如果是先取必败,则输出 lose

【输入样例】

3
3 6 9

【输出样例】

4 3
3 6 5

【核心思想】

  1. 问题分析:给定 \(k\) 堆火柴,每堆分别有 \(n_i\) 根。两名玩家轮流从某一堆中取走至少一根火柴,取走最后一根者获胜。需要判断先手是否必胜:若必败输出 lose;若必胜,输出字典序最小的第一步操作 \((b, a)\)(第 \(b\) 堆取走 \(a\) 根)以及操作后的状态。这是一个经典的 Nim 博弈 问题,胜负状态由所有堆大小的异或和决定。

  2. 算法选择

    • Nim 博弈定理(Bouton's Theorem):设 \(s = n_1 \oplus n_2 \oplus \cdots \oplus n_k\),若 \(s = 0\) 则先手必败,若 \(s \neq 0\) 则先手必胜。
    • Sprague-Grundy 函数思想:每堆火柴等价为一个独立子游戏,其 Grundy 值即为该堆火柴数 \(n_i\);整体游戏的 Grundy 值为各堆 Grundy 值的异或和。
    • 构造必胜操作:找到一堆 \(i\),使得 \((n_i \oplus s) < n_i\),将该堆火柴数变为 \(n_i \oplus s\),即可使新的异或和为 \(0\)
  3. 关键步骤

    • 读取输入并计算异或和:读入 \(k\) 和数组 \(n[1..k]\),计算 \(s = n_1 \oplus n_2 \oplus \cdots \oplus n_k\)
    • 判断胜负
      • \(s = 0\),先手处于必败态,直接输出 lose
      • \(s \neq 0\),先手存在必胜策略,继续构造第一步。
    • 构造字典序最小的必胜操作
      • 按堆号 \(i\) 从小到大遍历每堆火柴。
      • 对当前堆 \(i\),计算 \(t = n_i \oplus s\)
      • \(t < n_i\),说明可以从第 \(i\) 堆取走 \(a = n_i - t\) 根火柴,使该堆变为 \(t\),且新的整体异或和为 \(0\)
      • 由于按堆号从小到大扫描,第一个满足条件的 \(i\) 即为字典序最小的堆号 \(b\);而 \(a\)\(n_i\)\(s\) 唯一确定,因此 \((b, a)\) 即为所求字典序最小解。
    • 输出结果:第一行输出 \(a\)\(b\);第二行输出操作后的各堆火柴数状态。
  4. 时间/空间复杂度

    • 时间复杂度:\(O(k)\),需要一次遍历计算异或和,一次遍历寻找必胜操作。
    • 空间复杂度:\(O(k)\),用于存储每堆火柴数。
  5. 博弈论/Nim 博弈的核心思想

    • 异或和判定胜负:Nim 博弈中,所有堆大小的异或和 \(s\) 是状态的完全不变量;\(s = 0\) 对应 P-position(必败态),\(s \neq 0\) 对应 N-position(必胜态)。
    • 独立子游戏组合:多堆火柴可视为多个独立的一堆子游戏,整体 Grundy 值为各子游戏 Grundy 值的异或。
    • 构造转移使异或和为 0:从必胜态出发,总存在某一堆 \(i\),使得改变该堆大小后整体异或和变为 \(0\),从而将对手逼入必败态。
    • 最优策略的确定性:每次操作后都使对方面临异或和为 \(0\) 的状态,即可保证胜利。
    • 适用于多堆取物博弈、组合游戏胜负判定及第一步构造类问题。

【算法标签】

普及+ #博弈论

【代码详解】

#include <cstdio>
using namespace std;

int k, a[500002];  // k: 石子堆数, a: 每堆石子数

int main()
{
    scanf("%d", &k);  // 输入石子堆数
    int s = 0;  // 异或和
    for (int i = 1; i <= k; i++)  // 输入每堆石子数并计算异或和
    {
        scanf("%d", &a[i]);
        s ^= a[i];
    }

    if (!s)  // 如果异或和为0,先手必败
    {
        return puts("lose"), 0;  // 输出"lose"并结束
    }

    for (int i = 1; i <= k; i++)  // 找到必胜的一步
    {
        if ((a[i] ^ s) >= a[i])  // 如果操作后石子数增加或不变,跳过
        {
            continue;
        }
        printf("%d %d\n", (a[i] - (a[i] ^ s)), i);  // 输出取走的石子数和堆号
        a[i] = a[i] ^ s;  // 更新该堆石子数
        break;
    }

    for (int i = 1; i <= k; i++)  // 输出操作后的石子堆
    {
        printf("%d ", a[i]);
    }
    return 0;
}

【运行结果】

3
3 6 9
4 3
3 6 5
posted @ 2026-07-31 17:19  团爸讲算法  阅读(1)  评论(0)    收藏  举报