AcWing 1321. 取石子

\(AcWing\) \(1321\). 取石子

一、题目描述

\(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\)

六、实现代码

#include <bits/stdc++.h>
using namespace std;
const int N = 55, M = 50060;
int n, f[N][M];

int dfs(int a, int b) {
    if (~f[a][b]) return f[a][b]; // 记忆化搜索
    int &v = f[a][b];             // 引用,赋值更快捷
    if (!a) return b & 1;         // a==0时,看b是不是奇数,奇数必胜,否则必败

    if (b == 1) return dfs(a + 1, b - 1);
    if (a && !dfs(a - 1, b)) return v = 1;                          // 从左边取一石子:左侧不空,并且取完后整体是一个必败态,那我必胜
    if (b && !dfs(a, b - 1)) return v = 1;                          // 合并b
    if (a && b > 1 && !dfs(a - 1, b + 1)) return v = 1;             // 合并a,b各一个
    if (a > 1 && !dfs(a - 2, b == 0 ? b + 2 : b + 3)) return v = 1; // 合并a
    return v = 0;
}
int main() {
    int T;
    cin >> T;
    memset(f, -1, sizeof f); // 初始化DP数组,-1

    // 边界初始化
    f[1][0] = f[2][0] = 1;
    f[3][0] = 0;

    while (T--) {
        cin >> n;
        int a = 0, b = -1;
        for (int i = 1; i <= n; i++) {
            int x;
            cin >> x;
            if (x == 1)
                a++; // 左侧石子个数为1的石子堆数量
            else
                b += x + 1; // 1:新增加1堆,x:这一堆x个
        }
        if (b < 0) b = 0; // 一堆都没有,b=0

        if (dfs(a, b))
            puts("YES");
        else
            puts("NO");
    }
    return 0;
}
posted @ 2022-06-24 14:05  糖豆爸爸  阅读(79)  评论(0编辑  收藏  举报
Live2D