题目描述
输入一个长度为n的整数序列,从中找出一段长度不超过m的连续子序列,使得子序列中所有数的和最大。
注意: 子序列的长度至少是1。
输入格式
第一行输入两个整数n,m。
第二行输入n个数,代表长度为n的整数序列。
同一行数之间用空格隔开。
输出格式
输出一个整数,代表该序列的最大子序和。
数据范围
1 ≤ n , m ≤ 300000 1≤n,m≤300000 1≤n,m≤300000
输入样例
6 4
1 -3 5 1 -2 3
出样例
7
算法思想(前缀和+单调队列求滑动窗口最值)
根据题目描述,找出一段长度不超过m的连续子序列,使得子序列中所有数的和最大,属于最优化问题。
最优化问题一般可以描述为在一个有限集合中求最值,或者是方案数。比如背包问题,在一个有限容量( m m m)的背包中,对于 n n n个确定价值( w w w)和体积( v v v)的商品,对于商品的不同选择方案一定是一个有限集合。那么在这个有限集合中找到一个总价值最大的选择方案,就是从集合角度分析最优化问题。
对于本题来说,可以将长度为
n
n
n的整数序列中所有长度不超过
m
m
m的连续子序列看作一个全集,要求的是在这个全集中找到一个连续子序列和最大的序列。在求解之前需要将集合分类,分类的依据就是找到一个不同点,本题中可以根据序列中最后一个数在整数序列中的位置将集合划分成若干个子集,如下图所示:
那么,求出其中每一类的最大值,然后取其中最大的即为最终结果。
不妨以a[k]
结尾的子序列为例求该类的最大值,那么长度不超过m
的子序列如下图所示:
这m
个连续子序列的和可以利用前缀和求得,分别为:
s[k] - s[k - 1]
s[k] - s[k - 2]
s[k] - s[k - 3]
- …
s[k] - s[k - m]
所有长度为j
(
1
≤
j
≤
m
1\le j\le m
1≤j≤m)的连续子序列的和,都可以表示为s[k] - s[k - j]
。其中s[k]
是固定的,那么求最大值,只需要求k
之前大小为m
的区间中一个最小的s[i]
即可。这样本题可以转换为长度为m
的滑动窗口求最小前缀和问题,可以使用单调上升队列优化。
时间复杂度
单调队列求最值,时间复杂度为 O ( n ) O(n) O(n)。
代码实现
#include <iostream>
#include <cstdio>
using namespace std;
const int N = 300010, INF = 0x3f3f3f3f;
//q[]单调队列,存储的是数字在序列中的位置
int s[N], q[N];
int main()
{
int n, m;
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i ++)
{
scanf("%d", &s[i]);
s[i] += s[i - 1]; //计算前缀和
}
int hh = 0, tt = 0; //单调队列队头和队尾指针
q[tt] = 0; //因为要求前缀和的差,所以将0入队
int ans = -INF;
//滑动窗口求最值
for(int i = 1; i <= n; i ++)
{
//检查队头是否已滑出窗口
//此时i还没有入队,所以窗口长度为m - 1
if(hh <= tt && q[hh] + m < i) hh ++;
//打擂台求连续子序列和的最大值
ans = max(ans, s[i] - s[q[hh]]);
//单调队列优化,将队尾所有前缀和大于等于s[i]的元素出队
while(hh <= tt && s[q[tt]] >= s[i]) tt --;
//当前位置入队
q[++ tt] = i;
}
printf("%d\n", ans);
return 0;
}