|
|
|
|
##[$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>
|
|
|
|
|
|
|
|
|
|
### 三、本题题解
|
|
|
|
|
|
|
|
|
|
先来考虑 **简单情况**:所有堆的石子个数 $>1$
|
|
|
|
|
|
|
|
|
|
设 $b$ = 堆数 + 石子总数 $− 1$
|
|
|
|
|
|
|
|
|
|
可以推出: **先手必胜 $\Leftrightarrow$ $b$是奇数**
|
|
|
|
|
|
|
|
|
|
**证明**:考虑以下结论:
|
|
|
|
|
|
|
|
|
|
$α$. 任意一个是奇数的情况 **可以** 被转换成一个偶数的情况 (一定存在一个偶数后继)
|
|
|
|
|
|
|
|
|
|
$β$. 任意一个是偶数的情况 **一定** 会被转换成一个奇数的情况 (所有后继都是奇数)。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
先来考虑结论 $α.$
|
|
|
|
|
- 如果堆数 $>1$,可以合并其中两堆石子,这样 $b$ 就会变成偶数
|
|
|
|
|
- 如果堆数 $=1$,就可以拿掉 $1$ 个石子,这样 $b$ 也会变成偶数
|
|
|
|
|
|
|
|
|
|
再来考虑结论 $β$.
|
|
|
|
|
- 合并其中的两堆石子:显然,$b$ 会变成奇数
|
|
|
|
|
- 取了一个石子:
|
|
|
|
|
(1) 取得堆中石子个数 $>2$:石子个数 $− 1$,$b$ 会变成奇数
|
|
|
|
|
(2) 取得堆中石子个数 $=2$:石子个数变成了 $1$(继续分情况讨论):
|
|
|
|
|
1) 只有一堆:$b$ 是奇数,对手必胜
|
|
|
|
|
2) 多于一堆:$b$ 是奇数,对手可以把剩下的一个石子放到其他的堆里面去,对手同样必胜
|
|
|
|
|
|
|
|
|
|
这样,我们成功的证明除了: **先手必胜 $\Leftrightarrow $ ($b$ 是奇数)**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
接下来考虑一般情况:有些堆的石子个数可能 $=1$
|
|
|
|
|
假设石子个数 $=1$ 的堆数为 $a$,$b$ 仍然表示剩余石子的 **操作数**,即 (堆数 $+$ 石子总数 $− 1$)
|
|
|
|
|
把所有石子分成两个区域,一区域的数量 $=1$ (对应 $a$),二区域的数量都 $>1$ (对应 $b$).
|
|
|
|
|
|
|
|
|
|
那么可以写出以下转移:
|
|
|
|
|
|
|
|
|
|
$f(a,b)$:
|
|
|
|
|
|
|
|
|
|
① 从 $a$中取 $1$个 $→f(a−1,b)$
|
|
|
|
|
② 从 $b$中取 $1$个 $→f(a,b−1)$
|
|
|
|
|
③ 合并 $b$中的 $2$个 $→f(a,b−1)$
|
|
|
|
|
④ 合并 $a$中 $2$个 ($b$堆石子数 $+2$,堆数 $+1$) $→f(a−2,b+3)$
|
|
|
|
|
⑤ 合并 $a$中 $1$个 $b$中 $1$个 ($b$堆石子数 $+1$, $a$个数 $−1$)$→f(a−1,b+1)$
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#### $Code$
|
|
|
|
|
```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;
|
|
|
|
|
}
|
|
|
|
|
```
|