最大子序列和(连续的长度不超过 m 的非空子序列)
最大子序列和
后面发现这题其实是单调队列优化dp的模版题
题目链接:
https://loj.ac/p/10176
相关题目:
https://www.luogu.com/article/1r3wg4ki
给你一个长度为 n 的整数序列 \({A_1,A_2,\cdots ,A_n}\),要求从中找出一段连续的长度不超过 m 的非空子序列,使得这个序列的和最大。
输入格式
第一行为两个整数 n,m;
第二行为 n 个用空格分开的整数序列,每个数的绝对值都小于 1000。
输出格式
仅一个整数,表示连续长度不超过 m 的最大非空子序列和。
样例
输入复制
6 4
1 -3 5 1 -2 3
输出复制
7
数据范围与提示
对于 50% 的数据,\(1\le n,m\le 10^4\);
对于 100% 的数据,\(1\le n,m\le 2\times 10^5\)。
Response:
这题其实是 最大子段和问题 的一个变种,加了一个限制:子段长度不能超过 \(m\) 。
我们来逐步分析思路:
🔹 思路分析
-
最大子段和的经典解法
如果没有长度限制,经典做法是 Kadane 算法:\[dp[i] = \max(A[i], dp[i-1] + A[i]) \]其中 \(dp[i]\) = 以 第 \(i\) 个元素结尾 的连续子段的最大和
-
加上长度限制
现在子段长度要 \(\le m\) ,等价于:\[\max_{r} \Big( S[r] - \min_{r-m \le l-1 \le r-1} S[l-1] \Big) \]- \(S[r]\) 固定时,允许的左端点范围就是 \([r-m, r-1]\) 。
- 所以我们要在一个“滑动窗口”里找到最小的前缀和。
-
数据结构选择
为了高效维护窗口中的最小前缀和,可以用:- 单调队列(deque) 维护前缀和最小值;
🔹 算法步骤
- 计算前缀和数组 \(S\) ,其中 \(S[0]=0\) , \(S[i]=A_1+\cdots+A_i\) 。
- 用一个单调队列存储候选的 \(S[l-1]\) ,并保证队列中下标在合法范围 \([r-m, r-1]\) 。
- 枚举右端点 \(r\) ,计算:\[\text{ans} = \max(\text{ans}, S[r] - \min_{窗口} S[l-1]) \]
- 输出答案。
时间复杂度: \(O(n)\) 。
空间复杂度: \(O(n)\) 。
🔹 C++ 实现(OI 风格,含详细注释)
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 200000 + 5;
int n, m;
int a[N], S[N];
signed main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> a[i];
// 前缀和
S[0] = 0;
for (int i = 1; i <= n; i++) S[i] = S[i-1] + a[i];
deque<int> dq; // 存放候选下标 (维护前缀和最小值)
dq.push_back(0);// ***注意*** 初始可以选 l = 0
int ans = LLONG_MIN;
for (int r = 1; r <= n; r++) {
// 确保队首下标在合法范围内 (l-1 >= r-m)
while (!dq.empty() && dq.front() < r - m) dq.pop_front();
// 更新答案:S[r] - S[l-1]
if (!dq.empty())
ans = max(ans, S[r] - S[dq.front()]);
// ***这里注意,前面先更新答案,是后面才加入S[r], 这样就可以保证前面最小值不包含S[r],保证答案中下标在合法范围 $[r-m, r-1]
// 维护单调队列 (保证前缀和递增)
while (!dq.empty() && S[dq.back()] >= S[r]) dq.pop_back();
dq.push_back(r);
}
cout << ans << "\n";
return 0;
}

浙公网安备 33010602011771号