• AcWing 135. 最大子序和


    \(AcWing\) \(135\). 最大子序和

    题目传送门

    一、题目描述

    给定一个长度为 \(n\) 的序列 \(a\),找出其中 元素总和最大长度 不超过 \(m\)连续子区间

    二、先想一下暴力怎么做

    如果只要长度为\(m\)的连续子区间元素总和最大,那就是个简单的滑动窗口,我们先计算一下前缀和,然后维护一个滑动窗口,一边加来一边减就行了。

    此题就难度提升了,因为是一个 长度不超过\(m\)的连续子区间!

    窗口长度不固定,那我们就考虑枚举每个位置做为终点,然后从终点向回走,最长枚举\(m\)个长度,这样纯暴力的思路就出来了!

    暴力大法

    \(TL\)E掉\(3\)个点
    通过了 \(11/14\)个数据

    #include <cstdio>
    #include <cstring>
    #include <iostream>
    
    using namespace std;
    // Brute-force 暴力
    const int N = 300010;
    const int INF = 0x3f3f3f3f;
    int s[N];
    int n, m;
    
    //普通DP
    
    int main() {
        //加快读入
        ios::sync_with_stdio(false);
        cin.tie(0);
    
        cin >> n >> m;
        for (int i = 1; i <= n; i++) cin >> s[i], s[i] += s[i - 1];
    
        int res = -INF;
        //遍历每一个终点
        for (int i = 1; i <= n; i++)
            //从终点向前,找出所有区间在m之内的数字,通过前缀和计算出区间的累加和,保留最大值
            //在刚刚出发不久,长度不够m的时候,即i<m,则i-m会出现负数
            // j的物理含义:从i出发,倒序走到离i长度为m的范围内,
            // 栗子1:  i=10  m=3 ,有效的数据范围:j∈[10,9,8],最后面的8理解为i-m+1,也可写成j>i-m或j>=i-m+1
            // 上面的计算办法不是万能的,在起始边界会有问题:
            // 栗子2:  i=1  m=3 ,有效的数据范围:j∈[1],此时,再用i-m+1=1-3+1=-1就是无意义的,不可能遍历到小于数字1的下标
            // 所以,这个终止条件还要是细心记忆一下 j && j > i-m
            for (int j = i; j && j > i - m; j--)
                res = max(res, s[i] - s[j - 1]); // j>0 && j> i-m --> j> max(0,i-m)
    
        printf("%d\n", res);
        return 0;
    }
    

    三、想下怎么优化

    本题的数据范围是\(30w\),上面平方级别复杂度的代码显然会超时。

    状态表示

    \(f[i]\):以第\(i\)个数字结尾的长度不超过\(m\)的子序列的最大和

    \(s[i]\):数组的前缀和,则\(a[l] + ... + a[r] = s[r] - s[l-1]\)

    状态转移方程为\(f[i] = max(s[i] - s[j-1])\),其中\(i - j\)大于等于\(0\)并且不超过\(m\)

    一图胜千言:

    在单调队列的实现时,需要先在队列中加入哨兵结点\(s[0]\),后面就是解决四个问题了。

    • 第一,何时出队头,枚举到第\(i\)个数字时,第\(i\)个数字还没加入队列时,队头元素的下标允许的最小值是\(i - m\),所以当\(i - q[hh] > m\)时就需要出队头了;

    • 第二,何时出队尾,我们需要队头的元素是\(s[q[hh]]\)最小的元素,所以当\(s[i] <= s[q[tt]]\)时,出队尾元素;

    • 第三,何时加入新元素,队尾元素该出的出完了就可以将\(i\)加入到队列中了;

    • 第四,何时更新我们要求的长度不超过\(m\)的子序列的和的最大值\(res\),只要在确保队列中的元素个数不超过\(m\)时就可以尝试更新\(res\)了。

    时间复杂度
    使用单调队列优化\(DP\)的方法时间复杂度是\(O(n)\)

    四、哨兵实现方式

    #include <cstdio>
    #include <cstring>
    #include <iostream>
    
    using namespace std;
    const int N = 300010;
    const int INF = 0x3f3f3f3f;
    int n, m;
    int q[N];
    //单调队列,本质记录的是下标序号,因为只有记录了下标,才能知道是否离i的距离长度超过m了没有。
    //同时,记录了下标的话,想要其它的信息都是可以表示出来的,比如s[q[hh]]
    
    int s[N];       //前缀和
    int res = -INF; //预求最大,先设最小
    
    int main() {
        //加快读入
        ios::sync_with_stdio(false);
        cin.tie(0);
    
        cin >> n >> m;
        for (int i = 1; i <= n; i++) cin >> s[i], s[i] += s[i - 1]; //前缀和
    
        /*
         举个栗子:i=10,m=3 则应该是
         (1)m=3 ->8 9 10  a[8]+a[9]+a[10]=s[10]-s[7]
         (2)m=2 ->9 10    a[9]+a[10]=s[10]-s[8]
         (3)m=1 ->10      a[10]=s[10]-s[9]
         由于当前遍历到的是i=10,所以s[10]是固定值,变化的是后面的s[j], i-m<=j<=i-1
         我们在计算以10为终点的区间最大和时,队列中存入的应该是 7,8,9的索引号
         想要求最大值,就转化为求s[j]的最小值
         状态表示: s[i]-min(s[i-1],s[i-2],...,s[i-m])
         其中最小值的下标记录在维护好的单调队列队头中!
    
         这里需要考虑一下边界情况,比如i=1,也就是刚刚开始枚举第1个数,此时如果队列是空的,那么第1个数将无法
         取得队列头,因为队列是空的嘛!此时程序就会有问题,需要特判!这样太复杂了,一般的通用作法是:事先安排进去
         一个哨兵,它的作用就是解决边界情况,解决特判问题。
    
         哨兵的值视情况而定,比如本题,就是想解决第1个数字查询它前面的最小前缀和对应的索引号,也就是s[0]=0,
         我们直接把哨兵=0手动入入队列即可。
         */
    
        int hh = 0, tt = 0; //等价于增加了一个哨兵节点,也就是把下标为0的前缀和s[0]=0加入了滑动窗口中,描述1号节点向前m个节点的前缀和最小值是0
    
        // 枚举右边界
        for (int i = 1; i <= n; i++) {
            // 1、老掉牙的需要去世
            while (hh <= tt && q[hh] < i - m) hh++;
            // 2、此时单调队列中保存就是i以前、长度最长为m的单调上升的一个序列,队列头保存的就是i前面前缀和的最小值
            // 此时i还没有进入队列,单调队列其实在枚举数字i的左侧
            res = max(res, s[i] - s[q[hh]]);
            // 3、i入队列
            //比i老没它小的都去死吧~
            while (hh <= tt && s[q[tt]] >= s[i]) tt--;
            // i入队列,本轮结束
            q[++tt] = i;
        }
        //输出结果
        printf("%d\n", res);
        return 0;
    }
    

    五、基础课单调队列版本

    #include <iostream>
    
    using namespace std;
    const int INF = 0x3f3f3f3f;
    const int N = 3e5 + 10;
    
    int s[N], q[N];
    int hh, tt;
    int n, m;
    
    /*
    目标是s[i] - s[i-j], j = 1, 2, ..., m 的最大值 <=> 等价于求 s[i-j], j = 1, 2, ..., m 的最小值,
    即s的下标取区间 [i-m, i-1] 内的最小值,即滑动窗口求最小值问题,可以用单调队列来优化。
    
    y老师的代码与基础课的代码不大一样,实际上是把第一轮循环的更新操作放到循环之前去了,所以看起来有一点费解,
    这里给出一个与基础课对应的版本。
    */
    int main() {
        //加快读入
        ios::sync_with_stdio(false);
        cin.tie(0);
    
        cin >> n >> m;
        for (int i = 1; i <= n; i++) cin >> s[i], s[i] += s[i - 1];
    
        int res = -INF;
        /*
        1、我是i,我要计算max(s[i]-s[j]) j∈[i-1,i-m],此时s[i]是固定值,s[j]越小,值越大,转为求min(s[j]) j∈[i-1,i-m]
        2、在一个区间内的最小值,联想到单调队列维护区间最小值。
        3、需明确,是i左边最长宽度是m的一个窗口,还包括i
        4、讨论i时,需要保证的是i的前一位i-1应该已经加入到单调队列(就算它值大,不划算,但它最起码年轻,肯定得活下来)
            * 在保证前序加入到单调队列中时,单调队列最起码有一个元素,即s[i-1]存在
            * 这样的逻辑顺序就是在判断以i结尾的区间最大值时,先保证i-1在队列中,然后再利用单调队列的s[q[hh]]获取前m个长度窗口中的最小值
            * 总结:要想搝(qiu 3声),先得有!核心思想是保证单调队列中有东西,你才能去取,不能在边界时空着也硬去取~
        */
        hh = 0, tt = -1;
        for (int i = 1; i <= n; i++) { //枚举的是右边界
            // 1、保持队列长度
            while (hh <= tt && q[hh] < i - m) hh++; //[i-1 ~ i-m]
            // 2、i-1 号数据 进入队列,以保证后续s[i]可以通过s[q[hh]]获取到前序的最小值
            while (hh <= tt && s[q[tt]] >= s[i - 1]) tt--;
            q[++tt] = i - 1;
            // 3、区间最大值
            res = max(res, s[i] - s[q[hh]]);
        }
        //输出结果
        printf("%d\n", res);
        return 0;
    }
    
    

    六、疑惑的问题

    不知道别人啥情况,反正我在学习这道题时,一直很疑惑: 为什么要加哨兵,像基础课一样不加哨兵就不行吗?

    后来慢慢想明白了,现在解释一下:

    从实际含义解释

    我们准备用一个单调上升队列(维护最小值)来保存前缀和(保存的是下标),需要注意边界情况,比如第\(1\)个,按照计算式来讲,要想求它的区间和,需要用到\(s[1]\)减去滑动窗口的队头元素对应的前缀和,即\(s[1]-s[q[hh]]\),滑动窗口中保存的应该是\([i-m,i-1]\)的情况,但是因为它本身是第\(1\)个,它没有前序,窗口中没有数据,它还想去取,与理不合啊!

    我们的口号是:
    要想搝(\(qiu\) \(3\)声),先得有!

    核心思想:保证单调队列中有东西,你才能去取,不能在边界时空着也硬去取~

  • 相关阅读:
    ppt中调整图片位置
    如何理解 Google Protocol Buffer
    g++: error: unrecognized command line option ‘-std=C++11’
    手把手教你如何加入到github的开源世界!
    redis
    maven
    Spring----注释----开启Annotation <context:annotation-config> 和 <context:component-scan>诠释及区别
    JMX学习笔记(一)-MBean
    Redis学习笔记2-redis管道(pipeline)
    Redis学习笔记1-java 使用Redis(jedis)
  • 原文地址:https://www.cnblogs.com/littlehb/p/15801560.html
Copyright © 2020-2023  润新知