最大子序和(单调队列优化DP)

292 阅读2分钟

题目描述

输入一个长度为 nn 的整数序列,从中找出一段长度不超过 mm 的连续子序列,使得子序列中所有数的和最大。

注意:  子序列的长度至少是 11

输入格式

第一行输入两个整数 n,mn,m

第二行输入 nn 个数,代表长度为 nn 的整数序列。

同一行数之间用空格隔开。

输出格式

输出一个整数,代表该序列的最大子序和。

数据范围

1n,m3000001≤n,m≤300000,
保证所有输入和最终结果都在 intint 范围内。

输入样例:

6 4
1 -3 5 1 -2 3

输出样例:

7

题目分析

这是一道 单调数列优化DP 的题目。

首先要先将原数列转化为前缀和数列,这样对于每个子序列我们都可以以 O(1)O(1) 的复杂度得到区间和。

在暴力做法中,需要以 O(nm)O(nm) 的复杂度枚举子序列的左右端点,这是不可接受的。那么我们考虑只枚举右端点的情况来解决这个题。

定义 fif_i 表示以下标 i 结尾,且连续子序列长度不超过 m 的最大区间和。用数学式来表示:

fi=max{s[i]s[j]},        imj<if_i = max\{s[i] - s[j]\},\;\;\;\;i-m\le j<i

我们将此时的 s[i]s[i] 提出来,得到:

fi=s[i]min{s[j]},        imj<if_i = s[i] - min\{s[j]\},\;\;\;\;i-m\le j<i

即我们需要维护前 mms[j]s[j] 中的最小值,这里我们就可以采用单调队列优化的方式。即维护一个先入先出的队列,队首是在当前下标之前的 mmsjs_j 的最小值,在枚举当前坐标的过程中更新队头,具体过程见代码。

Accept代码

#include <bits/stdc++.h>

using namespace std;

const int N = 300010;

int s[N], q[N];
int hh, tt;     // 这里 hh = tt = 0,即一开始我们将下标 0 纳入队列

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];
    
    long long res = -1e10;
    for (int i = 1; i <= n; i ++)
    {
        if (q[hh] < i - m) hh ++;
        res = max(res, 1ll * s[i] - s[q[hh]]);
        while (hh <= tt && s[q[tt]] >= s[i]) tt --;     // 单调队列的核心代码
        q[++ tt] = i;
    }
    cout << res << "\n";
    return 0;
}