|
|
|
|
##[$AcWing$ $1321$. 取石子](https://www.acwing.com/problem/content/description/1323/)
|
|
|
|
|
|
|
|
|
|
### 一、题目描述
|
|
|
|
|
$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$
|
|
|
|
|
|
|
|
|
|
**输入样例:**
|
|
|
|
|
```cpp {.line-numbers}
|
|
|
|
|
3
|
|
|
|
|
3
|
|
|
|
|
1 1 2
|
|
|
|
|
2
|
|
|
|
|
3 4
|
|
|
|
|
3
|
|
|
|
|
2 3 5
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**输出样例:**
|
|
|
|
|
```cpp {.line-numbers}
|
|
|
|
|
YES
|
|
|
|
|
NO
|
|
|
|
|
NO
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 二、博弈论总结
|
|
|
|
|
<font size=4 color='red'><b>
|
|
|
|
|
必胜态 $\Rightarrow$ 选择合适方案 $\Rightarrow$ 对手必败态
|
|
|
|
|
必败态 $\Rightarrow$ 选择任何路线 $\Rightarrow$ 对手必胜态
|
|
|
|
|
</b></font>
|
|
|
|
|
|
|
|
|
|
对手聪明绝顶,不会犯错误,一旦他有机会获胜,他一定能找到合适的方案!所以,一定不能让他有机会,也就是总要让他总是处于必败状态,你才能获胜!
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### 三、思考过程
|
|
|
|
|
|
|
|
|
|
**$Q1$:本题中博弈的胜负与什么因素相关呢?**
|
|
|
|
|
**答**:因为只有两种操作:**拿走一个石子、合并两堆**,很显然, 两个关键因素: **石子个数、堆数**
|
|
|
|
|
|
|
|
|
|
**$Q2$:一般情况是什么,特殊情况是什么呢?**
|
|
|
|
|
**答**:如果某一堆石子只有$1$个,随着我们执行拿走$1$个的操作,它的堆就没了,这样石子个数变了,堆数也变了,一下变两个,问题变复杂了,上来就想难题,怕是搞不定。
|
|
|
|
|
既然这样,我们就思考一下 **一般情况** :只考虑所有个数大于等于$2$的那些堆,其它可能存在石子数等于$1$的,等我们想明白这个一般情况问题再研究特殊情况的事,由易到难。
|
|
|
|
|
|
|
|
|
|
**$Q3$:猜一下关联关系?**
|
|
|
|
|
两个操作同一时间只能执行一个,可以理解为拿走一个石子对结果影响一下,合并两堆石子对结果也是影响一下,初步考虑应该堆个数与石子总数的加法关系相关。
|
|
|
|
|
|
|
|
|
|
**一般情况:当每堆的石子个数都是大于等于$2$时,猜的关联关系**
|
|
|
|
|
|
|
|
|
|
<font size=5 color='red'><center><b>设$b$ = 堆数 + 石子总数 - $1$</b></center></font>
|
|
|
|
|
<font size=5 color='red'><center><b>结论:$b$是奇数⟺先手必胜,$b$是偶数⟺先手必败</b></center></font>
|
|
|
|
|
|
|
|
|
|
我们可以发现,当$n$是$1$的时候,这样结论显然成立。
|
|
|
|
|
|
|
|
|
|
然后分类讨论:
|
|
|
|
|
|
|
|
|
|
**情况$1$:没有数量为$1$的堆**
|
|
|
|
|
|
|
|
|
|
先证明奇数必胜,对于先手来说,
|
|
|
|
|
- $n>1$, 那么 **只要选两堆合并**, 那么 **总操作数变成偶数**
|
|
|
|
|
- $n=1$, 明显只能选择减少$1$操作,后手还是偶数。
|
|
|
|
|
|
|
|
|
|
对于后手来说,无论他是减少$1$,还是合并操作,留下的总操作数一定还是奇数。
|
|
|
|
|
|
|
|
|
|
对于某些读者来说,可能会问,如果后手把某个$2$变成$1$,先手该怎么办,其实这个很容易操作,如果堆数超过$1$,先手一定选择合并这个数量为$1$的堆,如果只有一堆了而且还是$1$,明显先手必胜了。 所以,先手总是有办法让后手必败(操作数为偶数的局面),后手无论怎么走,都会让先手必胜(变成操作为奇数的局面),所以我们证明成立。
|
|
|
|
|
|
|
|
|
|
在上面,我们也证明了当操作数是偶数的时候,先手是必败的。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**情况$2$:有数量为$1$的堆**
|
|
|
|
|
|
|
|
|
|
这个情况比较复杂,因为如果某个人把$1$减少$1$,那么这个堆同时也消失了,相当于操作数减少了$2$。
|
|
|
|
|
|
|
|
|
|
下面我们采用了动态规划的思想,用记忆化搜索来模拟演示一下:
|
|
|
|
|
|
|
|
|
|
$f[a][b]$ 表示当数量为$1$的堆有$a$个,剩下的堆的操作数是$b$的时候,先手是必胜还是必败,
|
|
|
|
|
f[a][b]=1 表示必胜,否则必败。$b$中不会出现数量为$1$的堆,除非只剩下一个堆了。
|
|
|
|
|
|
|
|
|
|
情况1: 当a>=2的时候,合并两个数量为1的堆,这样就让b这边的操作数增加了2+(b>0),因为如果原来b大于0,相当于操作数增加了3,否则b原来是0,那么操作是只增加2.
|
|
|
|
|
情况2: 和a>0 并且b>0 的时候,我们可以合并一个数量为1的堆和b中一个数量不为1的堆,那么a减少1,b增加1
|
|
|
|
|
情况3:b>=2,我们合并b里面的两个堆或者减少1,无论哪种,都是让b里面的操作数减少1。
|
|
|
|
|
情况4: 当a>0的时候,我们可以减少一个数量为1的堆,这样a就减少1,b不变。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
另外有一种非常特殊的情况,就是$b$等于$1$了,刚才我们说了,$b$里面不会出现数量为$1$的堆,除非只剩下一个堆了。因为$b$里面只要堆的数量超过$1$,就一定可以用合并超过替代减少$1$操作,这样是等价的。
|
|
|
|
|
|
|
|
|
|
除非$b$里面只有一个堆了,那么我们就只能不断减少$1$了。
|
|
|
|
|
|
|
|
|
|
所以当$b$是$1$的时候,实际我们求的问题应该变成$f[a+1][0]$
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### 六、实现代码
|
|
|
|
|
```cpp {.line-numbers}
|
|
|
|
|
#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;
|
|
|
|
|
}
|
|
|
|
|
```
|