前缀和加单调队列优化的一维 DP:转移拆成只含 j 的 dp[j-1]-S_j 与只含 i 的 S_i,在长度 K 的滑动窗口上均摊 O(1) 求最大值。
OJ: roj
题目 ID: 1599
难度:提高
标签:动态规划单调队列滑动窗口前缀和python
创建: 2026-09-30 20:52
更新: 2026-10-07 13:50
形式化题目
给定长度为
样例:
正解
思路
1. 朴素转移
记前缀和
- 不选:价值
; - 选:奶牛
必属于一个长度不超过 的连续段,设段左侧第一只不选的奶牛是 ( 表示从头开始选),则 且 ;奶牛 不选时方案完全落在前 只中,反之亦然,所以前缀 的最优值恰为 ,段内和为 。
于是
直接对每个
2. 拆项:窗口最大值
把转移中与
所以每个
- 过期:下标
的候选出队;下标逐个右移,每步至多一个过期,从队首弹; - 入队:算完
后候选 的值 就确定了,从队尾弹出所有不大于它的候选(它们下标更大、值更小,永远轮不到),再入队; - 查询:队首即窗口内
最大的下标,均摊 。
初值:
3. 样例 DP 表格
下表按 main.py 的状态定义逐步演算样例
| i | E_i | S_i | 窗口 j | 队首候选 | S_i+h[j] | dp[i-1] | dp[i] | 决策 |
|---|---|---|---|---|---|---|---|---|
| 1 | 1 | 1 | [0, 0] | j=0, h=0 | 1 | 0 | 1 | 选到 i(j=0,S_i+h=1) |
| 2 | 2 | 3 | [0, 1] | j=0, h=0 | 3 | 1 | 3 | 选到 i(j=0,S_i+h=3) |
| 3 | 3 | 6 | [1, 2] | j=1, h=-1 | 5 | 3 | 5 | 选到 i(j=1,S_i+h=5) |
| 4 | 4 | 10 | [2, 3] | j=2, h=-2 | 8 | 5 | 8 | 选到 i(j=2,S_i+h=8) |
| 5 | 5 | 15 | [3, 4] | j=3, h=-3 | 12 | 8 | 12 | 选到 i(j=3,S_i+h=12) |
观察三点:
代码与表列一一对应:window 存 prefix 是 dp_prev 是 value 是待入队的
代码
Python 版:
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-09-30 20:53
# update_at: 2026-09-30 20:53
import sys
from collections import deque
def max_efficiency(effs: list[int], k: int) -> int:
"""前缀和 + 单调队列求「不选超过 k 只连续奶牛」的最大效率和。
dp[i] = max(dp[i-1], S[i] + max_{i-k <= j <= i-1} (dp[j-1] - S[j])),
其中 j 是连续段左侧第一只不选的奶牛(j = 0 表示从头开始选),
窗口最大值由下标递增、候选值递减的双端队列维护。
"""
# (下标 j, 候选值 dp[j-1]-S[j]),队首最大;j=0 的候选值是 0 - S[0] = 0
window: deque[tuple[int, int]] = deque([(0, 0)])
prefix = 0 # S[i]:前 i 只奶牛的效率和
dp_prev = 0 # dp[i-1]:前 i-1 只奶牛的答案
for i, eff in enumerate(effs, 1):
prefix += eff # S[i] = S[i-1] + E_i
if window and window[0][0] < i - k: # 窗口左端 [i-k, i-1] 过期一个下标
window.popleft()
# 只有 k = 0 时窗口为空(一只都不许选),其余情况 dp[i-1] 必在候选里
dp_cur = prefix + window[0][1] if window else dp_prev
dp_cur = max(dp_cur, dp_prev) # 第 i 只不选,直接继承
# 入队候选 j = i:值为 dp[i-1] - S[i](奶牛 i 不选时,前缀最优值)
# 保持候选值递减,过期下标靠左端弹出,均摊 O(1)
value = dp_prev - prefix
while window and window[-1][1] <= value:
window.pop()
window.append((i, value))
dp_prev = dp_cur
return dp_prev
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
n, k = next(data), next(data)
effs = [next(data) for _ in range(n)]
print(max_efficiency(effs, k))
if __name__ == "__main__":
solve()C++ 版(同一算法):
/**
* Author by Rainboylv blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-05 11:52
* update_at: 2026-10-05 11:52
*/
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100005;
typedef long long ll;
ll n;
ll k;
ll e[MAXN]; // e[i]:第 i 只奶牛的效率
ll s[MAXN]; // s[i]:前缀和,s[i] = e[1] + ... + e[i]
ll dp[MAXN]; // dp[i]:前 i 只奶牛、无超过 k 只连续被选的最大效率和
ll q[MAXN]; // 单调队列存候选断点 j,队首到队尾下标递增、h 值递减
ll h(int j) { // 候选值 h(j) = dp[j-1] - s[j],只含 j,用于拆项
return dp[j - 1] - s[j];
}
// 单调队列优化的一维 DP,求无超过 k 连续被选奶牛的最大效率和。
void solve() {
s[0] = 0;
for (ll i = 1; i <= n; i++) s[i] = s[i - 1] + e[i];
// 先把候选 j=0(从头开始选,dp[-1] 视为 0)入队
int head = 1, tail = 1; // 队列区间 [head, tail),head==tail 表示空
q[tail++] = 0;
for (ll i = 1; i <= n; i++) {
// 队首过期:断点 j 必须满足 i - k <= j <= i - 1
while (head < tail && q[head] < i - k) head++;
// 选到 i:断点 j=i 时不选任何奶牛,对应 dp[i-1]
dp[i] = dp[i - 1];
if (head < tail) {
ll best = s[i] + h(q[head]);
if (best > dp[i]) dp[i] = best;
}
// 入队候选 j=i,队尾弹出所有 h 值不大于它的候选(下标更大、值更小,永远轮不到)
ll value = h(i);
while (head < tail && h(q[tail - 1]) <= value) tail--;
q[tail++] = i;
}
cout << dp[n] << "\n";
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> k;
for (ll i = 1; i <= n; i++) cin >> e[i];
solve();
return 0;
}复杂度
- 时间复杂度:
。读入 ;每只奶牛的候选恰好入队、出队各一次,队列操作均摊 ,DP 主循环每步 。对比朴素 的最坏 ,降为 量级的常数次运算。 - 空间复杂度:
。效率数组 (前缀和与 dp 只保留滚动变量),单调队列至多存 个下标;实测峰值内存 MB,远低于 128 MB 限制。
总结
- 模型是"选/不选"的一维 DP:转移的关键是把连续段左端点
理解为断点(奶牛 不选),状态价值取 而不是 ——写成 会把左右两段连成超过 的长段。 拆成"只含 "与"只含 "两部分,是队列优化 DP 的标准动作;此后每个 只剩一个滑动窗口最值问题。 - 单调队列保证:下标过期只在队首、劣质候选淘汰只在队尾,每个下标进出各一次,
因此变成 。 - 边界:
候选值 0 需显式入队; 时窗口为空,只继承 ,答案为 0。
图示解析
这张图串起本题从建模到得到答案的主线:
选子集、连续段长 <= K
`- 线性 DP:dp[i] 按奶牛 i 选/不选分两类
`- 拆项:dp[j-1]-S_j(只含 j)+ S_i(只含 i)
`- 单调队列维护窗口 [i-K, i-1] 的最大值
`- 每个下标进出各一次,O(N) 得到 dp[N]先看每个节点代表的推理结论:从"断点 j 不选"得到正确的状态价值,从"拆项"得到只与