• AcWing 1321. 取石子


    AcWing 1321. 取石子
    题目传送门

    一、博弈论总结


    1、必胜态 \(\Rightarrow\) 选择合适方案 $\Rightarrow $ 必败态,必败态$\Rightarrow \(选择任何路线\)\Rightarrow$ 必胜态

    2、执有必胜态的一方,采用某种策略,将对手处于必败状之中,此策略不能让对手有所选择

    二、简单情况

    为什么会想到讨论简单情况呢?我们来思考一下:如果某一堆石子只有\(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\)足够聪明的对手可以进行的操作分为两类:
      ① 如果只有这一堆石子,此时对方必胜;
      ② 如果有多堆石子,可以将这一个石子合并到其他堆中,这样每对石子个数都大于\(1\)


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

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

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

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

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

      先手后手一个一个取 \Rightarrow 先手败 &
      \end{matrix}\right.
      \
      从1中取一个& \Rightarrow {2,0} & 剩下一个一个取,先手败\
      合并两堆 & \Rightarrow {3} & 剩下一个一个取,先手胜 \
      \end{matrix}\right.

      \[指望对手出错我才有赢的机会,人家要是聪明,我就废了; 我是先手,我肯定不能把自己的命运交到别人手中!我选择合并两堆,这样我保准赢! * 把两堆直接合并,现在的状态$\{4,4\}$ 这下进入了我的套路,你取吧,你取一个,我也取一个;你再取一个,我也再取一个,结果,没有了,对手必败。 上面的例子可能不能描述所有场景,我现在$b$是奇数,我在必胜态,我不会让自己陷入到$b$可能是偶数的状态中去,如果我选择了 1、合并操作减少$1$个堆 2、拿走操作减少$1$个石子 都会把$b-1$这个偶数态给对方。 我不会傻到一个操作,可能造成堆也变化,石子个数也变化,这样就得看对方怎么选择了,而他还那么聪明,我不能犯这样的错误。 \]

    三、本题情况

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

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

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

    相关疑问
    \(Q1:\)情况\(3\)为什么是两个表达式?
    答:
    ①当右侧存在时,合并左边两堆石子,则右侧多出一堆石子,并且,石子个数增加\(2\),也就是\(b+=3\)
    ②当右侧一个都没有的时候,左边送来了一堆,两个石子,按\(b\)的定义,是堆数(1)+石子个数\((2)-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\),为什么没有看到这个状态变化呢?

    答:看一下 讨论简单情况中第\(3\)点后面的特殊情况,
    ① 如果右侧只有一堆,石子数量为\(2\),拿走\(1\)个,剩\(1\)个,一堆一个,对方必胜,此为必败态。

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


    四、时间复杂度

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


    五、实现代码

    #include <bits/stdc++.h>
    
    using namespace std;
    
    const int N = 55, M = 50050;
    
    int f[N][M];
    
    int dp(int a, int b) {
        int &v = f[a][b];
        if (v != -1) return v;
        if (!a) return v = b % 2;
        if (b == 1) return dp(a + 1, 0);
    
        if (a && !dp(a - 1, b)) return v = 1;
        if (b && !dp(a, b - 1)) return v = 1;
        if (a >= 2 && !dp(a - 2, b + (b ? 3 : 2))) return v = 1;
        if (a && b && !dp(a - 1, b + 1)) return v = 1;
    
        return v = 0;
    }
    
    int main() {
        memset(f, -1, sizeof f);
        int T;
        scanf("%d", &T);
        while (T--) {
            int n;
            scanf("%d", &n);
            int a = 0, b = 0;
            for (int i = 0; i < n; i++) {
                int x;
                scanf("%d", &x);
                if (x == 1)
                    a++;
                else
                    b += b ? x + 1 : x;
            }
    
            if (dp(a, b))
                puts("YES");
            else
                puts("NO");
        }
    
        return 0;
    }
    

  • 相关阅读:
    springcolud 的学习(一),架构的发展史
    shiro框架的学习
    Mybatis分页插件PageHelper简单使用
    对于解决VS2015启动界面卡在白屏的处理方法
    C# 运行流程
    转:什么是DIP、IoC、DI
    IQueryable,IEnumerable,IList区别
    easyUi——datetimebox绑定数据失效
    前后端参数传递的学习笔记
    java 多线程学习总结
  • 原文地址:https://www.cnblogs.com/littlehb/p/16408643.html
Copyright © 2020-2023  润新知