• AcWing 1087. 修剪草坪


    \(AcWing\) \(1087\). 修剪草坪

    题目传送门

    一、题目描述

    给定一个长度为 \(n\) 的数组 \(w\),其中 \(w_i\) 是第 \(i\) 个元素的 贡献

    我们可以选择的 数组 中的一些 元素,这些元素的 贡献总和 表示我们该种 方案 的 价值

    但是,如果方案中出现选择了 连续相邻 且超过 \(m\) 个元素,则这些 连续相邻 的元素 贡献 归零

    求解一种 方案,使得选择的 元素贡献总和 最大

    二、题目分析

    考虑用 动态规划 来求解本问题

    由于 连续选择 超过 \(m\) 个元素时,这些元素的 贡献\(0\) (相当于没选)

    而本题,所有的元素值都是 正整数,故我们的方案中,连续选择的元素数量 一定是 不超过\(m\)

    状态表示:
    \(f[i]\)为以\(i\)为结尾的所有数字中,合法(连续长度不超过\(m\)),并且贡献总和最大值。我们来分析一下这个最优状态可能由哪些状态转移而来。

    举个栗子,秒懂:

    假设\(m=3\),现在讨论一下第\(6\)头牛,它这个位置有两种选择,就是选与不选:

    • 不选:那么毫无疑问,由于第\(6\)头没有发挥作用,现在的最大贡献和前面的最大贡献和一致:\(f[6]=f[6-1]=f[5]\)

    • 选择:如果它选了,那么它前面的就不是随意的了,需要有范围限制,我们先用人脑模拟一下:
      最大连续选择长度不能超过\(3\),所以讨论以下情况:

      • 假设本轮取\(3\)个最优:

        \[\large f[6]=a[6]+a[5]+a[4]+ ? \]

      • 假设本轮取\(2\)个最优:

        \[\large f[6]=a[6]+a[5]+?' \]

      • 假设本轮取\(1\)个最优:

        \[\large f[6]=a[6]+?'' \]

    为啥要加\(?\)呢?以\(3\)个为例,如果取\(3\)个最优,那么此时取到了\(4\)号结点位置,\(3\)号肯定是不能取的!原因很简单,如果取上,就是连续\(4\)个了,超过了\(m\)限定!那\(3\)左侧是随意的,这时,\(f[2]\)的含义是在前\(2\)个元素中合法并可以获取到的最大值,现在的情况可以直接表示为\(f[2]\),同理得到:

    \(\large f[6]=a[6]+a[5]+a[4]+f[2] \\ \large f[6]=a[6]+a[5]+f[3] \\ \large f[6]=a[6]+f[4]\)

    \(a[6]+a[5]+a[4]\)这样的东东,很显然是前缀和的基本表示式,提示我们引入前缀和进行思考。如果预处理了前缀和,那么就有:

    利用前缀和思路优化一轮:

    \(\large f[6]=s[6]-s[3]+f[2]\)
    \(\large f[6]=s[6]-s[4]+f[3]\)
    \(\large f[6]=s[6]-s[5]+f[4]\)

    通用化处理一下:(\(6\)就是固定的当前值\(i\),而\(3,4,5\)是可变的,我们设为\(j\),同时\(j\)是有范围的,也就是\(i-m \sim i-1\))

    \[\large f[i]=s[i]-s[j]+f[j-1] ~~~ j \in [i-m,i-1] \]

    \(i\)确定的情况下,\(s[i]\)是固定的,变化的就是\(f[j-1]-s[j]\),要想求\(f[i]\)最大值,就是求\(f[j-1]-s[j]\)的最大值,而\(j\)是有范围的,这就转化为一个区间内取极值问题,可以用单调队列来优化

    单调队列优化办法

    • 我们维护一个队列,记录距离\(i\)前面的\(m\)个区间范*围内,\(f[j-1]-s[j]\)取值最大,就完成了优化

    • 这个最优的序号\(j\),一般就是队列头的记录序号\(q[hh]\)

    • 因为队列中不包含\(i\)结点,所以需要在\(i\)进入队列前,\(while\)上方进行计算更新结果。

    四、实现代码

    时间复杂度: \(O(n)\)

    #include <cstdio>
    #include <cstring>
    #include <iostream>
    
    using namespace std;
    const int N = 100010;
    typedef long long ll;
    int q[N];
    ll s[N], f[N];
    
    int main() {
        //加快读入
        ios::sync_with_stdio(false);
        cin.tie(0);
    
        int n, m;
        cin >> n >> m;
        for (int i = 1; i <= n; i++) cin >> s[i], s[i] += s[i - 1];
    
        //由于滑动窗口在i的左边,需要讨论前缀和的s[l-1],第1个没有前序,不符合整体的代码逻辑,添加了一个哨兵
        int hh = 0, tt = 0;
        for (int i = 1; i <= n; i++) {
            // 1、年龄大于m的老家伙们,不管是不是实力够强大,一概去死
            while (hh <= tt && i - q[hh] > m) hh++;
    
            //因为滑动窗口在i 左侧,先使用再加入
            f[i] = max(f[i - 1], f[max(0, q[hh] - 1)] + s[i] - s[q[hh]]);
    
            // 2、不如我年轻,并且,不如我有实力的老家伙们去死        
            while (hh <= tt && f[i - 1] - s[i] >= f[max(0, q[tt] - 1)] - s[q[tt]]) tt--;
    
            // 3、i入队列
            q[++tt] = i;
        }
        //输出结果
        printf("%lld\n", f[n]);
        return 0;
    }
    

    五、代码解读

    1、代码片段I

     f[i] = max(f[i - 1], f[max(0, q[hh] - 1)] + s[i] - s[q[hh]]);
    

    不选\(i\): \(f[i]=f[i-1]\)
    选择\(i\):
    看一下题解中总结的状态转移方程:\(f[i]=s[i]−s[j]+f[j−1]\)

    此时,单调队列\(q[hh]\)中保存的就应该是\(j\)的最优值,将\(j=q[hh]\)代入上式得到
    \(f[i]=s[i] -s[q[hh]]+f[q[hh]-1]\)
    可以选 也可以 不选,需要取\(max\)
    \(f[i] =max(f[i-1],s[i] -s[q[hh]]+f[q[hh]-1])\)
    这里有一个小技巧,或者说有一个小坑,就是\(f[q[hh]-1]\)
    我们知道,如果\(i=1\),就算我们加了哨兵进来,\(q[hh]=0\),那么\(0-1=-1\),这个是无法用数组下标的。
    为什么会出现这个问题呢?我们需要从现实意义出发去思考:
    \(f[k]\)的含义:在前\(k\)个元素中,连续长度不超过\(m\)的最大值,如果 \(i=1\)时,\(f[1]=s[1]-s[0]\)
    现在的式子是:\(s[i] -s[q[hh]]+f[q[hh]-1] =s[1]-s[0]+f[0-1]\),其实这种情况如果发现是负数,
    就直接给\(f[0]\)就行了,为啥直接给\(0\)呢?因为现实含义是:能取\(m\)个就一直向前尝试取\(m\)个,如果不够长,不能创造后再取吧?

    即:\(f[max(0,q[hh]-1)]\),如果实在想不明白,也可以拆开写:

      if (q[hh] - 1 >= 0)
          f[i] = max(f[i - 1], f[q[hh] - 1] + s[i] - s[q[hh]]);
      else
          f[i] = max(f[i - 1], s[i] - s[q[hh]]);
    

    2、代码片段II

     while (hh <= tt && f[i - 1] - s[i] >= f[max(0, q[tt] - 1)] - s[q[tt]]) tt--;
    

    \(Q:\)什么样的老东西需要去死呢?我们要思考一下我们与老家伙们PK的是什么内容:
    看一下题解中总结的状态转移方程:\(f[i]=s[i]−s[j]+f[j−1]\)
    \(i\)固定时,\(s[i]\)是定值,\(f[j-1]-s[j]\)是变化的,我们需要看看队列中的每个队尾开始的老家伙们,
    是不是这个值比如我小,比我还小,就没有必要继续活下去了。
    我是\(i\),我准备入队列,入队列的目的是给下一个\(i+1\)提供数据支撑,对于\(i+1\)而言,我其实就是\(j\)
    \(f[j-1]-s[j]==> f[i-1]-s[i]\) 就是\(i\)的本项\(PK\)对比取值
    队尾用\(q[tt]\)表示,也代入\(f[j-1]-s[j]=f[q[tt]-1]-s[q[tt]]\)
    也就是: \(f[i-1]-s[i] >= f[q[tt]-1]-s[q[tt]]\)的话,老家伙去死~
    这里面也有一个小坑坑,就是\(q[tt]-1\),有了上面的经验,我们知道可以直接\(max(0,q[tt]-1)\)即可

    六、复盘与总结

    • 动态规划的状态表示技巧
      \(f[i]\)为完成了前\(i\)个元素,并且合法,预示结果最大或最小的值。
    • 通过举栗子\(m=3,i=6\)来进行具体的思考,总结出一些规律后再进行通用化总结,人总是对具体的数字理解的快,对于抽象的公式理解的慢,我们就通过举栗子的方式来思考。
    • 尝试用\(f[j]+xx+yy\)的方式来表示\(f[i]\),来试图找出\(f[]\)数组之间的递推关系。
    • 可以尝试是否能用前缀和进行优化,这是一种非常常见的技巧。
    • 当讨论到\(i\)是,它的前缀和\(s[i]\)是固定值,我们一般对不确定值进行\(PK\),用单调队列记录离我在一定的距离内,最大或最小值。
    • 要注意数组下标的负数情况处理,从现实意义出发,思考负数是无意义的,当出现负数时,用\(0\)替代,即\(max(0,负数)\)
    • \(i\)入队列的目的是给下一个\(i+1\)提供支持,\(i+1\)\(i\)就是\(j\),所以,\(PK\)函数值时,\(i\)直接代入状态转移方程中\(j\)的位置即可。
  • 相关阅读:
    TCP/IP报文 三次握手 四次挥手
    socket 编程
    出现线程死锁的几种情况
    类模板的写法
    【HTTP】boundary 中一个 = 导致HTTP上传文件失败
    【时间戳】 年月日 转换为时间戳
    【CSV文件】CSV文件内容读取
    std::string 的方法c_str() 和 data() 有什么区别
    [转载] C++ STL中判断list为空,size()==0和empty()有什么区别
    【SQL】glob 和 like 的区别
  • 原文地址:https://www.cnblogs.com/littlehb/p/15812053.html
Copyright © 2020-2023  润新知