用单调队列维护能跳到当前格子的前驱 dp 最大值,在线性时间完成区间最优转移。
OJ: luogu
题目 ID: P1725
难度:普及+/提高-
标签:动态规划单调队列滑动窗口python
创建: 2025-12-26 19:37
更新: 2026-10-07 12:15
前置题目
luogu P1714切蛋糕B 的转移优化复用 A 教的「单调队列维护滑动窗口最值、先删除过期队头再加入新候选」这一步(dp[i]=A[i]+max(dp[j]) 的窗口最值就靠它),再叠加 A 未教的跳跃模型与「答案可从 i+R>N 直接跳出」的非固定终点处理。luogu P1440求m区间内的最小值B 的转移优化复用 A 的滑动窗口单调队列取最值这一步(过期前驱弹出后队头即窗口内 dp 最大位置),把 O(nR) 前驱枚举压成 O(n),再叠加 A 未教的 dp 转移设计与最后一步可越过 N 的答案收集。luogu P2032扫描B 把 A 教的「单调队列维护滑动窗口最大值」这一步直接用在 DP 转移 dp[i]=A[i]+max(dp[j]) 的前驱窗口上,再叠加最后一步可越过 N 时用 dp[i] 更新答案的边界处理。
后置题目
roj 3647跳房子B 的 check(g) 线性判定直接复用 A 教的『转移即滑动窗口最大值、用单调队列让队首恒为窗内最大 dp』这一步,只是把 A 的下标窗口 [i-R,i-L] 换成坐标窗口 [x_i-hi,x_i-lo];A 未教的二分答案与无解判据才是 B 新增的难度。roj 3571道路游戏A 教的「区间最值转移 = 滑动窗口最大值 → 单调队列存 (下标, dp 值)、弹队尾更差者、队头过期出队、队头即窗口最值」被 B 逐字复用:B 的 main.py 对每条对角线 c 的队列存 (k, dp[k]-P_c[k]-cost)、while q[-1][1]<=value 弹尾、q[0][0]<j-p 弹头,队头参与 dp[j]=P_c[j]+q[0][1],再在此之上叠加时间轴分段模型与对角线前缀和这两个 A 没有的新步骤。roj 3151环路运输B 在断环成链、把距离拆成 f(i)+g(j) 之后,直接沿用 A 教的单调队列:队尾弹出不更优者、队首弹出过期者,队首即窗口内最大值的左端点,只是窗口从 [i-R,i-L] 换成 [j-K,j-1]。roj 1601「一本通 5.5 例 5」BanknotesA 教的「转移只依赖区间内 dp 最值 → 滑动窗口 → 单调队列:入队时弹掉队尾更差者、队首先过期先出队」正是 B 的 relax() 对同余类窗口 [m-c,m] 求最小键 old[r+uw]-u 时实际使用的那一步,B 只是先做同余类拆分与换元把多重背包转移变成定长滑窗,再套同一套队列不变量。roj 1603「一本通 5.5 练习 2」绿色通道B 的可行性判定 DP 直接复用 A 教的单调队列滑动窗口最值转移,只是把窗口内 dp 最大值换成最小值,再在外层叠加二分答案roj 1605「一本通 5.5 练习 4」股票交易B 正解的买入/卖出转移就是把 A 教的「用单调队列保存前驱下标、取合法前驱窗口内 dp 最大值」搬到 f[p][j']+price*j' 上,再叠加 W 天前驱快照与拆项这两个新步骤。roj 1602「一本通 5.5 练习 1」烽火传递A 教的「转移只依赖区间内 dp 最值 → 滑动窗口 → 单调队列保存前驱下标、队首最值、过期出队并弹掉队尾劣者」正是 B 计算 dp[i] = a_i + min(dp[i-m..i-1]) 时实际使用的那一步;B 只是先做约束改写(相邻点火点距离 ≤ m 加尾部豁免)把问题化成滑动窗口最小值,再套用同一队列不变量。roj 1599「一本通 5.5 例 3」修剪草坪A 教的是「转移需要区间内 dp 最大值 → 滑动窗口 → 单调队列队首即答案」,B 把这一步原样搬到窗口 [i-K,i-1] 上,只是先把候选值由 dp[j] 拆项成 h(j)=dp[j-1]-S_j,队列的过期出队与劣质淘汰机制完全一致。luogu U663797疯狂的背包问题(10) - 多重背包问题 IIIB 的多重背包单调队列优化直接复用 A 教的滑窗单调队列三步(过期下标出队、弹掉队尾更差元素、用队头求最优),只是先把容量按体积余数分组成等差数列,使同一套队头转移能 O(1) 取每组窗口最优前驱。luogu P3594[POI 2015 R3] 狼坑 Trous de loupB 的双指针窗口里复用 A 的单调队列取窗口最值机制,队头维护当前窗口内长度恰好 d 的子段和最大值luogu P3572[POI 2014] PTA-Little BirdB 的 main.cpp 原样复用 A 教的滑动窗口单调队列三步(弹过期、取队头最优前驱、单调插入),只是把窗口换成 [i-k,i-1]、把队内比较从「dp 最小」扩成「先比 dp 再比高度」。luogu P2569[SCOI2010] 股票交易B 的买入、卖出转移把 a_step 的滑动窗口单调队列原样套用,只是窗口从 P1725 的「跳来的前驱下标」换成「持股数 k 区间」。luogu P3522[POI 2011] TEM-TemperatureB 的最长合法区间步骤复用 A 教的单调队列滑动窗口取最值(队首弹过期、队首即窗内最值),这里换成维护窗口内最小 bad_pos,再叠加冲突对转化与树状数组求 bad_posluogu P5665[CSP-S 2019] 划分B 的满分解法把 A 教的单调队列取最优前驱原样搬到划分 DP 上:P1725 用队列在滑动窗口内取 dp 最大前驱,P5665 把「上一段段和不超过最后一段」化成 key(t)<=c[i] 的单调性判据后,同样靠队头取最优、队尾弹掉更差候选来 O(1) 得到最靠后的可行断点,只在 A 之上多出正数序列贪心与 key 的推导。luogu P2254[NOI2005] 瑰丽华尔兹A 教的“合法前驱是右移滑动窗口、用单调队列维护窗口内最大值”正是 B main.cpp 里 move_right/up 等四个函数在做的转移(old_dp-下标 的滑动窗口取最大),B 只是把 1D 下标窗口搬到网格行/列并叠加上整段压缩、-k 变换与家具清空队列。luogu P2851[USACO06DEC] The Fewest Coins GB 的多重背包优化直接复用 A 教的滑动窗口单调队列:A 用队列在 [i-R,i-L] 内取 dp 最大值,B 对每个 mod v 余数类维护 old[t*v+r]-t 的长度 c+1 窗口最小值,把 O(k) 枚举降为 O(1) 取队头。
题意
有编号 0..N 的格子,起点在 0,每次可以从 i 跳到 [i+L, i+R] 中的任意格子。停在格子 i 可以获得 A[i],当下一步跳到大于 N 的位置时算到达对岸。求能获得的最大冰冻指数。
思路
设 dp[i] 表示到达格子 i 时能获得的最大冰冻指数。想转移到 i,前一个格子 j 必须满足:
text
i - R <= j <= i - L朴素做法是对每个 i 枚举所有合法前驱 j。
先看一个可以直接验证想法的朴素解:
cpp
#include <bits/stdc++.h>
using namespace std;
// brute.cpp:直接枚举每个格子的所有前驱,复杂度 O(nr),只适合小数据。
const int MAXN = 305;
const int NEG_INF = -1000000000;
int n, l, r;
int a[MAXN];
int dp[MAXN];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> l >> r;
for (int i = 0; i <= n; i++) {
cin >> a[i];
dp[i] = NEG_INF;
}
dp[0] = 0;
int ans = NEG_INF;
for (int i = 1; i <= n; i++) {
for (int j = max(0, i - r); j <= i - l; j++) {
if (dp[j] != NEG_INF) {
dp[i] = max(dp[i], dp[j] + a[i]);
}
}
if (i + r > n) {
ans = max(ans, dp[i]);
}
}
cout << ans << '\n';
return 0;
}朴素做法最坏接近 N=2 * 10^5。
观察转移:
text
dp[i] = A[i] + max(dp[j]) (i-R <= j <= i-L)这就是滑动窗口最大值。随着 i 增大,合法前驱区间整体向右滑动。用单调队列保存前驱下标,并让队头始终是当前窗口内 dp 最大的位置。
每一轮处理 i 时:
- 把新进入窗口的
i-L加入队列; - 删除小于
i-R的过期前驱; - 若队列非空,用队头更新
dp[i]; - 如果
i+R>N,说明可以从i一步跳到对岸,用dp[i]更新答案。
注意答案不是固定的 dp[N],因为最后一步可以直接跳过 N。
样例中
格子 i |
合法前驱区间 | 可达前驱最大值 | A[i] |
dp[i] |
|---|---|---|---|---|
| 2 | [0,0] |
0 | 3 | 3 |
| 3 | [0,1] |
0 | 11 | 11 |
| 4 | [1,2] |
3 | 7 | 10 |
| 5 | [2,3] |
11 | -2 | 9 |
队列维护第三列的窗口最大值;格子 3 可以再跳 3 格越过 dp[3]=11。
Python 知识
deque保存候选下标,队头对应当前最大dp。array("q")紧凑保存权值和 DP,负无穷使用 64 位范围内的-(1 << 60)。- 先加入
i-L、再删除小于i-R的下标,和合法前驱区间严格对应。
代码
python
import sys
from array import array
from collections import deque
data = iter(map(int, sys.stdin.buffer.read().split()))
n, left_jump, right_jump = next(data), next(data), next(data)
value = array("q", (next(data) for _ in range(n + 1)))
negative_infinity = -(1 << 60)
dp = array("q", [negative_infinity]) * (n + 1)
dp[0] = 0
queue = deque()
answer = negative_infinity
for i in range(left_jump, n + 1):
candidate = i - left_jump
if dp[candidate] != negative_infinity:
while queue and dp[queue[-1]] <= dp[candidate]:
queue.pop()
queue.append(candidate)
while queue and queue[0] < i - right_jump:
queue.popleft()
if queue:
dp[i] = dp[queue[0]] + value[i]
if i + right_jump > n:
answer = max(answer, dp[i])
print(answer)复杂度
时间复杂度
空间复杂度
总结
本题的核心是把“能从哪些点跳到当前点”反推成区间 [i-R, i-L]。只要看出转移需要这个区间内的 dp 最大值,就可以用单调队列优化。