单调队列优化DP
单调队列在 DP 中的基本应用,是对这样一类 DP 状态转移方程进行优化:
其本质原因是外层
如图,
例题:P1725 琪露诺
解题思路
设
单调队列进出队怎么做?队首出界的是
注意:
参考代码
#include <cstdio> #include <deque> using namespace std; const int N = 2e5 + 5; const int INF = 2e9; int a[N], dp[N]; int main() { int n, l, r; scanf("%d%d%d", &n, &l, &r); for (int i = 0; i <= n; i++) scanf("%d", &a[i]); for (int i = 1; i <= n; i++) dp[i] = -INF; deque<int> q; for (int i = l; i <= n; i++) { while (!q.empty() && q.front() < i - r) q.pop_front(); while (!q.empty() && dp[q.back()] < dp[i - l]) q.pop_back(); q.push_back(i - l); // 当上一步的窗口中全是不可达状态时说明当前位置也不可达 if (dp[q.front()] == -INF) continue; dp[i] = dp[q.front()] + a[i]; } int ans = -INF; for (int i = n - r + 1; i <= n; i++) ans = max(ans, dp[i]); printf("%d\n", ans); return 0; }
例题:P3957 [NOIP2017 普及组] 跳房子
解题思路
首先注意到
二分答案
设
随着
每次把与当前点距离
时间复杂度为
参考代码
#include <cstdio> #include <deque> #include <algorithm> using namespace std; typedef long long LL; const int N = 500005; const LL INF = 1e12; int x[N], s[N], n, d, k; LL dp[N]; bool check(int g) { for (int i = 1; i <= n; i++) dp[i] = -INF; deque<int> q; int minstep = max(d - g, 1); int j = 0; for (int i = 1; i <= n; i++) { while (!q.empty() && x[q.front()] < x[i] - d - g) q.pop_front(); while (j < i && x[j] <= x[i] - minstep) { if (x[j] < x[i] - d - g) { j++; continue; } while (!q.empty() && dp[q.back()] < dp[j]) q.pop_back(); q.push_back(j); j++; } if (q.empty() || dp[q.front()] == -INF) continue; dp[i] = dp[q.front()] + s[i]; if (dp[i] >= k) return true; } return false; } int main() { scanf("%d%d%d", &n, &d, &k); LL pos = 0; for (int i = 1; i <= n; i++) { scanf("%d%d", &x[i], &s[i]); if (s[i] > 0) pos += s[i]; } if (pos >= k) { int l = 0, r = max(d, x[n]), ans; while (l <= r) { int mid = (l + r) / 2; if (check(mid)) { r = mid - 1; ans = mid; } else { l = mid + 1; } } printf("%d\n", ans); } else printf("-1\n"); return 0; }
例题:P1776 宝物筛选(单调队列优化多重背包)
解题思路
假设物品的重量是
将式子中的
将转移方程拆成两个部分(跟转移点有关、跟当前点有关),得到
为了保证
总时间复杂度为
参考代码
#include <cstdio> #include <algorithm> #include <deque> const int W = 40005; int dp[2][W]; int main() { int n, maxw; scanf("%d%d", &n, &maxw); for (int i = 1; i <= n; i++) { int cur = i & 1, pre = 1 - cur; int v, w, c; scanf("%d%d%d", &v, &w, &c); int lim = std::min(1ll * maxw, 1ll * c * w); for (int r = 0; r < w && r <= maxw; r++) { // 真正的j对w取余后的数 std::deque<int> dq; for (int j = r; j <= maxw; j += w) { while (!dq.empty() && dq.front() < j - lim) dq.pop_front(); while (!dq.empty()) { if (dp[pre][j] - dp[pre][dq.back()] >= (j - dq.back()) / w * v) { dq.pop_back(); } else break; } dq.push_back(j); dp[cur][j] = dp[pre][dq.front()] + (j - dq.front()) / w * v; } } } printf("%d\n", dp[n & 1][maxw]); return 0; }
拓展:多重背包的方案数问题与可行性问题
方案数问题(没法用二进制优化):恰好占了
,其中 且- 还是把
按 分类,维护滑动窗口中 的和(直接用一个变量维护即可)
可行性问题(bool
类型):哪些重量是能凑出来的?哪些是不能的?
,其中 且- 有多少个
是true
- 用一个变量
cnt
维护一下
例题:P3800 Power收集
给定一个
数据范围:
分析:记
这个转移实际上就是滑动窗口问题,即区间查询的左端点和右端点都是单调的。每一行
#include <cstdio> #include <deque> #include <algorithm> using namespace std; const int N = 4005; int a[N][N], maxv[N][N], dp[N][N]; int main() { int n, m, k, t; scanf("%d%d%d%d", &n, &m, &k, &t); while (k--) { int x, y, v; scanf("%d%d%d", &x, &y, &v); a[x][y] = v; } for (int i = 1; i <= n; i++) { for (int j = 1; j <= m; j++) dp[i][j] = maxv[i - 1][j] + a[i][j]; deque<int> dq; int l = 1, r = 1; for (int j = 1; j <= m; j++) { while (!dq.empty() && dq.front() < j - t) dq.pop_front(); while (r <= j + t && r <= m) { while (!dq.empty() && dp[i][dq.back()] <= dp[i][r]) dq.pop_back(); dq.push_back(r); r++; } maxv[i][j] = dp[i][dq.front()]; } } int ans = 0; for (int i = 1; i <= m; i++) ans = max(ans, dp[n][i]); printf("%d\n", ans); return 0; }
例题:P2627 [USACO11OPEN] Mowing the Lawn G
问题描述:有一个包括
个正整数的序列,第 个整数为 ,给定一个整数 ,找这样的子序列,子序列中的数在原序列连续不能超过 个。对子序列求和,问所有子序列中最大的和是多少? 。
例如:,原序列为 , ,选择 和 ,其最大和为 ,其中每一段连续长度都不超过 。
分析:设
在计算
参考代码
#include <cstdio> #include <algorithm> #include <deque> using namespace std; typedef long long LL; const int N = 100005; LL dp[N], sum[N]; int e[N]; LL calc(int i) { // 技巧:队列中只记录下标,需要比较实际的大小时再代入计算 return i == 0 ? 0 : dp[i - 1] - sum[i]; } int main() { int n, k; scanf("%d%d", &n, &k); for (int i = 1; i <= n; i++) { scanf("%d", &e[i]); sum[i] = sum[i - 1] + e[i]; } deque<int> dq; dq.push_back(0); for (int i = 1; i <= n; i++) { // dp[i] = max(dp[j-1]-sum[j])+sum[i] j in [i-k,i] while (!dq.empty() && dq.front() < i - k) dq.pop_front(); while (!dq.empty() && calc(dq.back()) <= calc(i)) dq.pop_back(); dq.push_back(i); dp[i] = calc(dq.front()) + sum[i]; } printf("%lld\n", dp[n]); return 0; }
例题:CF1918D Blocking Elements
解题思路
考虑二分答案。因为尝试的分隔代价限定得越小,就越难实现,限定得越大则越有可能实现,满足单调性。
当尝试的分隔代价限定为
时间复杂度为
参考代码
#include <cstdio> #include <deque> using namespace std; typedef long long LL; const int N = 100005; int a[N], n; LL dp[N], sum[N]; bool check(LL x) { deque<int> dq; dq.push_back(0); int idx = 0; for (int i = 1; i <= n + 1; i++) { while (idx <= i && sum[i - 1] - sum[idx] > x) idx++; while (!dq.empty() && dq.front() < idx) dq.pop_front(); dp[i] = dp[dq.front()] + a[i]; while (!dq.empty() && dp[dq.back()] >= dp[i]) dq.pop_back(); dq.push_back(i); } return dp[n + 1] <= x; } int main() { int t; scanf("%d", &t); while (t--) { scanf("%d", &n); for (int i = 1; i <= n; i++) { scanf("%d", &a[i]); sum[i] = sum[i - 1] + a[i]; } a[n + 1] = 0; sum[n + 1] = sum[n]; LL ans, l = 1, r = sum[n]; while (l <= r) { LL mid = (l + r) / 2; if (check(mid)) { r = mid - 1; ans = mid; } else l = mid + 1; } printf("%lld\n", ans); } return 0; }
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战