[学习笔记] 单调队列优化DP - DP
单调队列优化DP
简单好想的DP优化
真正的教育是把学过的知识忘掉后剩下的东西 —— ***
对于一个转移方程类似于 的DP,如果暴力实现的话复杂度是 ,实现方法是双层for循环嵌套。但如果区间 与区间 存在交集,或者说当 变化时,不同的 所对应的 区间存在重叠,那么我们在使用 进行遍历时就会产生重复计算,而单调队列优化DP就是解决这一重复计算的法宝。
如何用单调队列进行优化呢?可以将 所在的区间看作一个滑动窗口,每次循环 的时候将元素进队,并且更新 的值(找到合法区间),这样就可以将每次寻找最大值的时间复杂度均摊为 ,再加上dp的 次转移,时间复杂度为 。完美~
使用这一优化方法的前提是: max(min)里的东西必须只与 相关,不然没办法优化。
单调队列优化多重背包
我们知道多重背包的朴素DP表达式为:,其中 。但是这个式子和单调队列优化DP的普通形式 差太多了,无法直接用单调队列优化。
考虑到单调队列优化的前提是存在重复的计算,显然有 和 在计算时存在重复计算。那么也就是说,当 时是存在重复计算的,那么问题就很清楚了。
令 、,那么 。于是有:
令 ,则有:
这个DP式即可进行单调队列优化。
就一般的题目而言,只要是蓝题及以上的,满足单调队列优化的狮子都不会太明显,需要一步一步去转化。优化多重背包就是个很巧妙的转化的例子。掌握这类转化的技巧对这类DP很有帮助。
例题
「一本通 5.5 练习 1」烽火传递
纯纯的单调队列优化DP。建议打通这道题,对后面的理解很有帮助。
根据题目可知转移方程为:,其中 。为了看得清楚,把min里面和 有关的东西全都踢出去:。那么就可以建立一个关于dp
的单调队列,在每次计算dp[i]
前要先把dp[i-1]
入队。最后答案为 中的dp最大值。
#include<bits/stdc++.h> using namespace std; const int N = 2e5 + 5; int n, m, a[N], dp[N], p[N], tail, head=1, ans = INT_MAX; int main(){ ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); cin>>n>>m; for(int i=1; i<=n; ++i) cin>>a[i]; for(int i=1; i<=n; ++i){ while(tail >= head && dp[p[tail]] >= dp[i-1]) --tail; // 关于dp数组的单调队列 p[++tail] = i-1; if(p[head] < i-m) ++head; dp[i] = dp[p[head]] + a[i]; if(i > n-m) ans = min(ans, dp[i]); } return cout<<ans, 0; }
「一本通 5.5 例 2」最大连续和
纯纯的单调队列题。根据题目可知转移方程(假了)为:,其中 。转化为能看懂的 。用单调队列求解即可。复杂度 。
#include<bits/stdc++.h> using namespace std; const int N = 2e5 + 5; int n, m, sum[N], p[N], tail, head = 1, ans = INT_MIN; int main(){ ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); cin>>n>>m; for(int i=1, a; i<=n; ++i) cin>>a, sum[i] = sum[i-1] + a; for(int i=1; i<=n; ++i){ while(tail >= head && sum[p[tail]] >= sum[i-1]) --tail; p[++tail] = i-1; while(p[head] < i-m) ++head; ans = max(ans, sum[i]-sum[p[head]]); } return cout<<ans, 0; }
[USACO11OPEN] Mowing the Lawn G
很好的单调队列DP入门题。设 dp[i]
表示选择第 项元素的合法序列的最大和。那么可得转移方程 。 表示前缀和。其中 ,这里的 可以理解为两段连续区间的断开处,并且 是可以等于 的。但是如果DP包含了 ,那必然会涉及 的计算。不妨在整个序列前加入一个 ,在进行DP,那么就可以解决这一问题。
#include<bits/stdc++.h> using namespace std; #define int long long const int N = 2e5 + 5; int n, k, dp[N], sum[N], p[N], tail=1, head=0, ans = INT_MIN; signed main(){ ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); cin>>n>>k; for(int i=2, a; i<=n+1; ++i) cin>>a, sum[i] = sum[i-1] + a; for(int i=1; i<=n+1; ++i){ while(tail > head && dp[p[tail]-1]-sum[p[tail]] <= dp[i-1]-sum[i]) --tail; p[++tail] = i; while(p[head] < i-k) ++head; dp[i] = dp[p[head]-1] - sum[p[head]] + sum[i]; ans = max(ans, dp[i]); } return cout<<ans, 0; }
[POI2005] BAN-Bank Notes
下面只给出计算最小硬币数的代码,方案数略去。
#include<bits/stdc++.h> using namespace std; const int N = 2e4 + 1; int n, k, dp[N], c[N], m[N], p[N], num[N], tail, head; int main(){ ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); cin>>n; for(int i=1; i<=n; ++i) cin>>c[i]; for(int i=1; i<=n; ++i) cin>>m[i]; cin>>k; for(int i=1; i<=k; ++i) dp[i] = INT_MAX; for(int i=1; i<=n; ++i){ if(m[i] > k / c[i]) m[i] = k/c[i]; for(int b=0; b<c[i]; ++b){ tail = 0, head = 1; for(int y=0; y<=(k-b)/c[i]; ++y){ int tmp = dp[b + y*c[i]] - y; while(tail >= head && p[tail] >= tmp) --tail; p[++tail] = tmp, num[tail] = y; while(head <= tail && num[head] < y-m[i]) ++head; dp[b+y*c[i]] = min(dp[b+y*c[i]], p[head] + y); } } } return cout<<dp[k], 0; }
[SCOI2010] 股票交易
这道题的题目非常的繁琐啊,看的人眼花缭乱的。不过如果你注意力十分充沛的话就可以发现,这道题其实和背包DP很像。我们可以列出总的DP转移方程式:令 表示第 天拥有 支股票的最大收益。则有:
但似乎这狮子又臭又长,没法处理,那么考虑分开来转移:先处理卖出股票的转移,再处理买入股票的转移。先看卖出股票:
令 则有:
处理成功!同理,卖出股票的转移方程也是一样:
考虑到最开始时手里没有股票,所以需要先全部买入,再进行后面的转移。当然我们也可以选择什么也不做直接由昨天转移到今天。
#include<bits/stdc++.h> using namespace std; const int N = 2e3 + 1; int T, MaxP, w, AP, BP, AS, BS, dp[N][N], p[N], num[N], head, tail, ans = INT_MIN; int main(){ ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); cin>>T>>MaxP>>w; memset(dp, -0x7f, sizeof dp); for(int i=1; i<=T; ++i){ cin>>AP>>BP>>AS>>BS; for(int j=0; j<=min(AS, MaxP); ++j) dp[i][j] = -j * AP; //全部买入 for(int j=0; j<=MaxP; ++j) dp[i][j] = max(dp[i][j], dp[i-1][j]); //什么也不做 if(i <= w) continue; tail = 0, head = 1; for(int j=0; j<=MaxP; ++j){ //买入 int tmp = dp[i-w-1][j] + j*AP; while(tail >= head && p[tail] <= tmp) --tail; p[++tail] = tmp, num[tail] = j; while(head <= tail && num[head] < j-min(j, AS)) ++head; dp[i][j] = max(dp[i][j], p[head]-j*AP); } tail = 0, head = 1; for(int j=MaxP; j>=0; --j){ // 卖出 int tmp = dp[i-w-1][j] + j*BP; while(tail >= head && p[tail] <= tmp) --tail; p[++tail] = tmp, num[tail] = j; while(head <= tail && num[head] > min(MaxP-j, BS)+j) ++head; dp[i][j] = max(dp[i][j], p[head]-j*BP); } } return cout<<dp[T][0], 0; }
[NOI2005] 瑰丽华尔兹
披着暴力外衣的DP。跟魔法没半毛钱关系。
首先,定义状态 dp[i][j]
表示当前第 行第 列所走的最大距离。然后全部初始化为 -inf
,这样就可以保证转移出来数字一定是能走到的地方。因为要从最开始的地方进行转移,那么把 dp[x][y]
设为 。
接着考虑转移,我们可以根据方向把整张图都转移一遍,比如方向为 那就从下往上刷。如果刷到障碍物,就单调队列归零,continue跳过障碍物重新开始刷。这样就可以保证DP值为正的地方就是能走的地方。然后ans取max即可。
四个方向的DP转移方程式如下:
用单调队列优化后即可得到 的转移复杂度。总复杂度为 。
#include<bits/stdc++.h> using namespace std; int n, m, x, y, K, dmax, dir, num[201], tail, head; char ch; bitset<201> G[201]; long long ans, dp[201][201], p[201], tmp; int main(){ ios::sync_with_stdio(0), cin.tie(0) ,cout.tie(0); cin>>n>>m>>x>>y>>K; for(int i=1; i<=n; ++i) for(int j=1; j<=m; ++j){ cin>>ch; if(ch == 'x') G[i][j] = 1; } memset(dp, -0x7f, sizeof dp); dp[x][y] = 0; for(int i=1, l, r; i<=K; ++i){ cin>>l>>r>>dir; dmax = r-l+1; if(dir == 3) for(int j=1; j<=n; ++j){ tail = 0, head = 1; for(int k=m; k>=1; --k){ if(G[j][k]){tail = 0, head = 1; continue; } tmp = dp[j][k] + k; while(tail >= head && p[tail] <= tmp) --tail; p[++tail] = tmp, num[tail] = k; while(head <= tail && num[head] > min(m, k+dmax)) ++head; dp[j][k] = max(dp[j][k], p[head]-k); ans = max(ans, dp[j][k]); } }else if(dir == 4) for(int j=1; j<=n; ++j){ tail = 0, head = 1; for(int k=1; k<=m; ++k){ if(G[j][k]){tail = 0, head = 1; continue; } tmp = dp[j][k] - k; while(tail >= head && p[tail] <= tmp) --tail; p[++tail] = tmp, num[tail] = k; while(head <= tail && num[head] < k-min(k, dmax)) ++head; dp[j][k] = max(dp[j][k], p[head]+k); ans = max(ans, dp[j][k]); } }else if(dir == 1) for(int j=1; j<=m; ++j){ tail = 0, head = 1; for(int k=n; k>=1; --k){ if(G[k][j]){tail = 0, head = 1; continue; } tmp = dp[k][j] + k; while(tail >= head && p[tail] <= tmp) --tail; p[++tail] = tmp, num[tail] = k; while(head <= tail && num[head] > min(n, dmax+k)) ++head; dp[k][j] =max(dp[k][j], p[head]-k); ans = max(ans, dp[k][j]); } }else if(dir == 2) for(int j=1; j<=m; ++j){ tail = 0, head = 1; for(int k=1; k<=n; ++k){ if(G[k][j]){tail = 0, head = 1; continue; } tmp = dp[k][j] - k; while(tail >= head && p[tail] <= tmp) --tail; p[++tail] = tmp, num[tail] = k; while(head <= tail && num[head] < k-min(k, dmax)) ++head; dp[k][j] = max(dp[k][j], p[head]+k); ans = max(ans, dp[k][j]); } } } return cout<<ans, 0; }
[USACO13NOV] Pogo-Cow S
神题好吧。一般单调队列是固定左右端点移动中间的 值,这个是固定中间的 值不断扩展左右端点。
定义状态 dp[j][i]
表示从第 个点跳到第 个点的最大分数。
#include<bits/stdc++.h> using namespace std; int n, dp[1001][1001], ans; struct target{ int x, p; }tg[1001]; bool cmp(target a, target b){ if(a.x < b.x) return 1; return 0; } int main(){ ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); cin>>n; for(int i=1; i<=n; ++i) cin>>tg[i].x>>tg[i].p; sort(tg+1, tg+n+1, cmp); for(int j=1; j<=n; ++j){ dp[0][j] = dp[j][j] = tg[j].p; for(int i=j+1, h=j+1; i<=n; ++i){ dp[j][i] = dp[j][i-1] - tg[i-1].p; while(h >= 0 && tg[i].x - tg[j].x >= tg[j].x - tg[h-1].x) dp[j][i] = max(dp[j][i], dp[--h][j]); dp[j][i] += tg[i].p; ans = max(ans, dp[j][i]); } } for(int j=n; j>=1; --j){ dp[j][0] = tg[j].p; for(int i=j-1, h=j-1; i>0; --i){ dp[j][i] = dp[j][i+1] - tg[i+1].p; while(h <= n && tg[j].x - tg[i].x >= tg[h+1].x - tg[j].x) dp[j][i] = max(dp[j][i], dp[++h][j]); dp[j][i] += tg[i].p; ans = max(ans, dp[j][i]); } } return cout<<ans, 0; }
本文作者:XiaoLe_MC
本文链接:https://www.cnblogs.com/xiaolemc/p/18225077
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步