7.3 KiB
一、题目描述
Alice
和 Bob
两个好朋友又开始玩取石子了。
游戏开始时,有 N
堆石子排成一排,然后他们轮流操作(Alice
先手),每次操作时从下面的规则中任选一个:
- 从某堆石子中取走一个;
- 合并任意两堆石子。
不能操作的人输。
Alice
想知道,她是否能有必胜策略。
输入格式
第一行输入 T
,表示数据组数。
对于每组测试数据,第一行读入 N
;
接下来 N
个正整数 a_1,a_2,⋯,a_N
,表示每堆石子的数量。
输出格式 对于每组测试数据,输出一行。
输出 YES
表示 Alice
有必胜策略,输出 NO
表示 Alice
没有必胜策略。
数据范围
1≤T≤100,1≤N≤50,1≤a_i≤1000
输入样例:
3
3
1 1 2
2
3 4
3
2 3 5
输出样例:
YES
NO
NO
二、博弈论总结
必胜态 \Rightarrow
选择合适方案 \Rightarrow
对手必败态
必败态 \Rightarrow
选择任何路线 \Rightarrow
对手必胜态
对手聪明绝顶,不会犯错误,一旦他有机会获胜,他一定能找到合适的方案!所以,一定不能让他有机会,也就是总要让他总是处于必败状态,你才能获胜!
三、思考过程
Q1
:本题中博弈的胜负与什么因素相关呢?
答:因为只有两种操作:拿走一个石子、合并两堆,很显然, 两个关键因素: 石子个数、堆数
Q2
:一般情况是什么,特殊情况是什么呢?
答:如果某一堆石子只有1
个,随着我们执行拿走1
个的操作,它的堆就没了,这样石子个数变了,堆数也变了,一下变两个,问题变复杂了,上来就想难题,怕是搞不定。
既然这样,我们就思考一下 一般情况 :只考虑所有个数大于等于2
的那些堆,其它可能存在石子数等于1
的,等我们想明白这个一般情况问题再研究特殊情况的事,由易到难。
Q3
:猜一下关联关系?
两个操作同一时间只能执行一个,可以理解为拿走一个石子对结果影响一下,合并两堆石子对结果也是影响一下,初步考虑应该堆个数与石子总数的加法关系相关。
一般情况:当每堆的石子个数都是大于等于2
时,猜关联关系
b
= 堆数 + 石子总数 - 1
结论:b
是奇数⟺先手必胜,b
是偶数⟺先手必败
我们可以发现,当n
是1
的时候,也就是只有1
堆时,比如a_0=3
,那么b=3+1-1=3
,是奇数:
- ① 先手拿走一个,剩操作数=
2
- ② 后手只能拿走
1
个,剩操作数1
- ③ 先手再拿走1个,剩余操作数
=0
- ④ 后手没有可以拿的了,后手负,先手必胜!
结论显然成立。
当不只有1
堆时,分类讨论:
情况1
:没有数量为1
的堆
先证明奇数必胜,对于先手来说,
n>1
, 那么 只要选两堆合并, 那么 总操作数变成偶数n=1
, 明显只能选择减少1
操作,后手还是偶数。
对于后手来说,无论他是减少1
,还是合并操作,留下的总操作数一定还是奇数。
对于某些读者来说,可能会问,如果后手把某个2
变成1
,先手该怎么办,其实这个很容易操作,如果堆数超过1
,先手一定选择合并这个数量为1
的堆,如果只有一堆了而且还是1
,明显先手必胜了。 所以,先手总是有办法让后手必败(操作数为偶数的局面),后手无论怎么走,都会让先手必胜(变成操作为奇数的局面),所以我们证明成立。
在上面,我们也证明了当操作数是偶数的时候,先手是必败的。
情况2
:有数量为1
的堆
这个情况比较复杂,因为如果某个人把1
减少1
,那么这个堆同时也消失了,相当于操作数减少了2
。
- 假设有
a
堆石子,其中每堆石子个数为1
- 剩余堆的石子个数都严格大于
1
根据这些数量大于1
的堆的石子可以求出上述定义出的b
,我们使用f(a, b)
表示此时先手必胜还是必败,因为博弈论在本质上是可以递推的,我们可以想出起点,再想出递推关系,就可以递推得到更大数据情况下的递推值,也就是博弈论本质上是dp
。

Q:
情况3
为什么是两个表达式?
答:
①当右侧存在时,合并左边两堆石子,则右侧多出一堆石子,并且,石子个数增加2
,也就是b+=3
②当右侧一个都没有的时候,左边送来了一堆,两个石子,按b
的定义,是堆数+石子个数-1=2
,即b+=2
六、实现代码
#include <bits/stdc++.h>
using namespace std;
const int N = 55, M = 50050; // M 包括了 50 * 1000 + 50个石子数量为1的堆数
int f[N][M];
int dfs(int a, int b) {
int &v = f[a][b];
if (~v) return v;
// 简单情况: 即所有堆的石子个数都是严格大于1,此时a是0
if (!a) return v = b % 2; // 奇数为先手必胜,偶数为先手必败
// 一般5个情况 + 1个特殊情况
// 特殊情况: 如果操作后出现b中只有一堆,且堆中石子个数为1
// 那么应该归入到a中,并且b为0
// 以下所有情况,如果能进入必败态,先手则必胜!
if (b == 1) return dfs(a + 1, 0);
// 情况1:有a,从a中取一个
if (a && !dfs(a - 1, b)) return v = 1;
// 情况2, 4:有b,从b中取1个(石子总数 - 1) or 合并b中两堆(堆数 - 1),
if (b && !dfs(a, b - 1)) return v = 1;
// 情况3:有a >= 2, 合并a中两个
// 如果b的堆数不为0, a - 2, b + 1堆 + 2个石子(只需要加delta) ====> b + 3
// 如果b的堆数为0, a - 2, 0 + 2个石子 + 1堆 - 1 ====> b + 2
if (a >= 2 && !dfs(a - 2, b + (b ? 3 : 2))) return v = 1;
// 情况5:有a,有b, 合并a中1个b中1个, a - 1, b的堆数无变化 + 1个石子(只加delta)
if (a && b && !dfs(a - 1, b + 1)) return v = 1;
// 其他情况,则先手处于必败状态
return v = 0;
}
int main() {
memset(f, -1, sizeof f);
int T, n;
cin >> T;
while (T--) {
cin >> n;
int a = 0, b = 0;
for (int i = 0; i < n; i++) {
int x;
cin >> x;
if (x == 1) a++;
// b != 0时 加1堆 + 加x石子 = 原来的 + x + 1 (其实就是区别一开始的时候)
// 当b != 0时, 我们往后加的delta
// b == 0时 加1堆 + 加x石子 = 0 + 1 + x - 1 = x
// 注意操作符优先级
else
b += b ? x + 1 : x;
}
// 1 为先手必胜, 0为先手必败
if (dfs(a, b))
puts("YES");
else
puts("NO");
}
return 0;
}