You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

11 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

##AcWing 1321. 取石子

参考题解

一、题目描述

AliceBob 两个好朋友又开始玩取石子了。

游戏开始时,有 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 必胜态

三、简单情况

为什么会想到讨论简单情况呢?我们来思考一下:如果某一堆石子只有1个,随着我们执行拿走1个的操作,它的堆就没了,这样石子个数变了,堆数也变了,两个变量,问题变复杂了,我们上来就想难题,怕是搞不定。

既然这样,我们就思考一下 子空间 :只考虑所有个数大于等于2的那些堆,其它可能存在石子数等于1的,等我们想明白这个简单问题再研究扩展的事,由易到难。

同时,我们需要思考博弈的胜负与什么因素相关呢?因为只有两种操作:拿走一个石子、合并两堆,很显然,两个关键因素:石子个数、堆数

同时,两个操作同一时间只能执行一个,所以可以理解为拿走一个石子对结果影响一下,合并两堆石子对结果也是影响一下,初步考虑应该堆个数与石子总数的加法关系相关。

子空间:当每堆的石子个数都是大于等于2

b = 堆数 + 石子总数 - 1 结论:b是奇数⟺先手必胜,b是偶数⟺先手必败

证明:

1、边界当我们只有一堆石子且该堆石子个数为1个时,b=1,先手必胜。

2、当b为奇数,一定可以通过某种操作将b变成偶数

  • 如果堆数大于1,合并两堆让b变为偶数
  • 如果堆数等于1,从该堆石子中取出一个就可以让b变为偶数

3、当b为偶数,无论如何操作,b都必将变为奇数

  • 合并两堆,则b变为奇数
  • 从某一堆中取走一个石子:
    • 若该堆石子个数大于2,则b变为奇数,且所有堆石子数量严格大于1
    • 若该堆石子个数等于2,取一个石子后,b变为奇数,该堆石子个数变为1个,此时就再是子空间范围内了,因为出现某堆的石子个数为1,而不是每一堆都大于等于2了!需要继续分类讨论:

#### 特殊情况
此时为了保证所有堆的石子个数大于1足够聪明的对手 可以进行的操作分为两类: ① 如果只有这一堆石子,此时 对手必胜 ② 如果有多堆石子,可以将这一个石子合并到其他堆中,这样每对石子个数都大于1

Q:对手为什么一定要采用合并的操作,而不是从别的堆中取石子呢? 我来举两个简单的栗子:

  • 只有一堆石子 石子个数是2个。你拿走一个,对手直接拿走另一个,游戏结束,对手赢了!你也是足够聪明的,你会在这种情况下这么拿吗?不能吧~,啥时候可能遇到这个情况呢?就是你被 逼到 这个场景下,也就是一直处于必败态!

  • 两堆石子 每堆石子个数是2个。我是先手,可以有两种选择:

    (1)、从任意一堆中拿走1个, 现在的局面是\{2,1\}

    \large 后手选择(对手) \Rightarrow 
     \left\{\begin{matrix}
     从2中取一个 &  \Rightarrow  \{1,1\}  & \Rightarrow 
     \large \left\{\begin{matrix}
    

先手合并 \Rightarrow {2}& 剩下一个一个取,先手胜 \ 先手后手一个一个取 \Rightarrow 先手败 & \end{matrix}\right.
\ 从1中取一个& \Rightarrow {2,0} & 剩下一个一个取,先手败\ 合并两堆 & \Rightarrow {3} & 剩下一个一个取,先手胜 \ \end{matrix}\right. 指望对手出错我才有赢的机会,人家要是聪明,我就废了!

我是先手,我肯定不能把自己的命运交到别人手中!我选择合并两堆,这样我保准赢!

(2)、把两堆直接合并,现在的状态$\{4\}$
这下进入了我的套路,你取吧,你取一个,我也取一个;你再取一个,我也再取一个,结果,没有了,**对手必败**。

上面的例子可能不能描述所有场景,我现在b是奇数,我在必胜态,我不会让自己陷入到b可能是偶数的状态中去,如果我选择了

  • 合并操作减少1个堆
  • 拿走操作减少1个石子 都会把b-1这个偶数态给对方

我不会傻到一个操作,即可能造成堆也变化,让石子个数也变化,这样就得看对方怎么选择了,而他还那么聪明,我不能犯这样的错误。

四、本题情况

本题中可能存在一些堆的石子个数等于1:

  • 假设有a堆石子,其中每堆石子个数为1
  • 剩余堆的石子个数都严格大于1

根据这些数量大于1的堆的石子可以求出上述定义出的b,我们使用f(a, b)表示此时先手必胜还是必败,因为博弈论在本质上是可以递推的,我们可以想出起点,再想出递推关系,就可以递推得到更大数据情况下的递推值,也就是博弈论本质上是dp

相关疑问 Q1:情况3为什么是两个表达式? 答: ①当右侧存在时,合并左边两堆石子,则右侧多出一堆石子,并且,石子个数增加2,也就是b+=3

②当右侧一个都没有的时候,左边送来了一堆,两个石子,按b的定义,是堆数+石子个数-1=2,即b+=2

Q2为什么用一个奇数来描述简单情况的状态,而不是用偶数呢? 答:因为要通过递推式进行计算,最终的边界是需要我们考虑的:

  • 如果用奇数,那么边界就是b=1,表示只有1堆,石子数量只有1个,此时当然必胜。

  • 如果用偶数,比如边界是b=0,表示目前0堆,0个石子,这都啥也没有了,还必胜态,不符合逻辑,说不清道不明。

  • 那要是不用b=0做边界,用b=2呢?表示只有1堆,石子数量只有1个,这个应该也是可以,但没有再仔细想了。

Q3:情况2从右边取一个石子,如果此时右侧存在某一堆中石子个数是2,取走1个后,变成了1,不就是右侧减少了一个堆,减少了两个石子,即b-=3;同时,此堆石子个数变为1,左侧个数a+=1,为什么没有看到这个状态变化呢?

答:这是因为聪明人不会从右侧某个石子数量大于2的堆中取走石子!

看一下 讨论简单情况 中第3点后面的 特殊情况:

  • 如果右侧只有一堆,石子数量为2,拿走1个,剩1个,一堆一个,对方必胜,此为必败态

  • 如果右侧大于一堆,某一堆只有2个石子,拿走1个,剩1个,对手足够聪明,会采用右侧两堆合并的办法,此时 石子数量减1,堆数减1,对b的影响是减2,对b的奇偶性没有影响,换句话说,如果你现在处在必败态,你这么整完,还是必败态

五、时间复杂度

这里因为a最大取50b最大取50050,因此计算这些状态的计算量为2.5×10^6,虽然有最多100次查询,但是这些状态每个只会计算一遍,因此不会超时。

六、实现代码

#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;
}