题解:洛谷 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
【核心思想】
-
问题分析:给定 \(k\) 堆火柴,每堆分别有 \(n_i\) 根。两名玩家轮流从某一堆中取走至少一根火柴,取走最后一根者获胜。需要判断先手是否必胜:若必败输出
lose;若必胜,输出字典序最小的第一步操作 \((b, a)\)(第 \(b\) 堆取走 \(a\) 根)以及操作后的状态。这是一个经典的 Nim 博弈 问题,胜负状态由所有堆大小的异或和决定。 -
算法选择:
- 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\)。
-
关键步骤:
- 读取输入并计算异或和:读入 \(k\) 和数组 \(n[1..k]\),计算 \(s = n_1 \oplus n_2 \oplus \cdots \oplus n_k\)。
- 判断胜负:
- 若 \(s = 0\),先手处于必败态,直接输出
lose。 - 若 \(s \neq 0\),先手存在必胜策略,继续构造第一步。
- 若 \(s = 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\);第二行输出操作后的各堆火柴数状态。
-
时间/空间复杂度:
- 时间复杂度:\(O(k)\),需要一次遍历计算异或和,一次遍历寻找必胜操作。
- 空间复杂度:\(O(k)\),用于存储每堆火柴数。
-
博弈论/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
浙公网安备 33010602011771号