单调队列优化DP

简单好想的DP优化

真正的教育是把学过的知识忘掉后剩下的东西 —— ***

对于一个转移方程类似于 \(dp[i]=max(min)\{dp[j]+b[j]+a[i]\}\ \ x_i<=j<=y_i\) 的DP,如果暴力实现的话复杂度是 \(O(n^2)\),实现方法是双层for循环嵌套。但如果区间 \([x_i,y_i]\) 与区间 \([x_{i+1},y_{i+1}]\) 存在交集,或者说当 \(i\) 变化时,不同的 \(i\) 所对应的 \(j\) 区间存在重叠,那么我们在使用 \(j\) 进行遍历时就会产生重复计算,而单调队列优化DP就是解决这一重复计算的法宝。

如何用单调队列进行优化呢?可以将 \(j\) 所在的区间看作一个滑动窗口,每次循环 \(i\) 的时候将元素进队,并且更新 \(head\) 的值(找到合法区间),这样就可以将每次寻找最大值的时间复杂度均摊为 \(O(1)\),再加上dp的 \(n\) 次转移,时间复杂度为 \(O(1)*O(n)=O(n)\)。完美~

使用这一优化方法的前提是: max(min)里的东西必须只与 \(j\) 相关,不然没办法优化。

单调队列优化多重背包

我们知道多重背包的朴素DP表达式为:\(dp[j]=max\{dp[j-k*c_i]+k*w_i\}\),其中 \(0\leqslant k\leqslant min\{m_i,j/c_i\}\)。但是这个式子和单调队列优化DP的普通形式 \(dp[i]=max\{dp[i]+b[j] \}+a[i]\ \ \ L(i)\leqslant j\leqslant R(i)\) 差太多了,无法直接用单调队列优化。

考虑到单调队列优化的前提是存在重复的计算,显然有 \(j\) 和 \(j+c_i\) 在计算时存在重复计算。那么也就是说,当\(j_1\equiv j_2\ mod\ c_i\) 时是存在重复计算的,那么问题就很清楚了。

令 \(b=j\%c_i\)、\(y=j/c_i\),那么 \(j=b+y*c_i\)。于是有:

\[\begin{split}
dp[b+y*c_i]=max\{dp[b+(y-k)*c_i]+k*w_i\}
\end{split}
\]

令 \(x=y-k\),则有:

\[\begin{split}
dp[b+y*c_i]&=max\{dp[b+x*c_i]+(y-x)*w_i\}\\
&=max\{dp[b+x*c_i]-x*w_i\}+y*w_i\\
&y-min\{m_i, y\}\leqslant x\leqslant y
\end{split}
\]

这个DP式即可进行单调队列优化。

就一般的题目而言,只要是蓝题及以上的,满足单调队列优化的狮子都不会太明显,需要一步一步去转化。优化多重背包就是个很巧妙的转化的例子。掌握这类转化的技巧对这类DP很有帮助。

例题

「一本通 5.5 练习 1」烽火传递

纯纯的单调队列优化DP。建议打通这道题,对后面的理解很有帮助。

根据题目可知转移方程为:\(dp[i]=min\{dp[j]+a[i]\}\),其中 \(j\in[i-m, i-1]\)。为了看得清楚,把min里面和 \(i\) 有关的东西全都踢出去:\(dp[i]=min\{dp[j]\}+a[i]\)。那么就可以建立一个关于dp的单调队列,在每次计算dp[i]前要先把dp[i-1]入队。最后答案为 \(i\in[n-m+1,n]\) 中的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」最大连续和

纯纯的单调队列题。根据题目可知转移方程(假了)为:\(dp[i]=max\{sum[i]-sum[j]\}\),其中 \(j\in[i-m,i-1]\)。转化为能看懂的 \(dp[i]=sum[i]-min\{sum[j]\}\)。用单调队列求解即可。复杂度 \(O(n)\)。

#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] 表示选择第 \(i\) 项元素的合法序列的最大和。那么可得转移方程 \(dp[i]=max\{dp[j-1]-sum[j]\}+sum[i]\) 。\(sum[]\) 表示前缀和。其中 \(j\in[i-m,i-1]\),这里的 \(j\) 可以理解为两段连续区间的断开处,并且 \(j\) 是可以等于 \(0\) 的。但是如果DP包含了 \(0\),那必然会涉及 \(dp[-1]\) 的计算。不妨在整个序列前加入一个 \(0\),在进行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转移方程式:令 \(dp[i][j]\) 表示第 \(i\) 天拥有 \(j\) 支股票的最大收益。则有:

\[\begin{split}
dp[i][j]=max\{dp[i-w-1][j-a+b]-a*AP_i+b*BP_i \}
\end{split}
\]

但似乎这狮子又臭又长,没法处理,那么考虑分开来转移:先处理卖出股票的转移,再处理买入股票的转移。先看卖出股票:

\[\begin{split}
dp[i][j]=max&\{dp[i-w-1][j-a]-a*AP_i\}\\
&0\leqslant a\leqslant min\{j,AS\}
\end{split}
\]

令 \(k=j-a\) 则有:

\[\begin{split}
dp[i][j]=max&\{dp[i-w-1][k]+k*AP_i\}-j*AP_i\\
&j-min\{j,AS\}\leqslant k\leqslant j
\end{split}
\]

处理成功!同理,卖出股票的转移方程也是一样:

\[\begin{split}
dp[i][j]=max&\{dp[i-w-1][k]+k*BP_i\}-j*BP_i\\
&j\leqslant k\leqslant min\{MaxP-j,BS\}+j
\end{split}
\]

考虑到最开始时手里没有股票,所以需要先全部买入,再进行后面的转移。当然我们也可以选择什么也不做直接由昨天转移到今天。

#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] 表示当前第 \(i\) 行第 \(j\) 列所走的最大距离。然后全部初始化为 -inf,这样就可以保证转移出来数字一定是能走到的地方。因为要从最开始的地方进行转移,那么把 dp[x][y] 设为 \(0\)。

接着考虑转移,我们可以根据方向把整张图都转移一遍,比如方向为 \(1\) 那就从下往上刷。如果刷到障碍物,就单调队列归零,continue跳过障碍物重新开始刷。这样就可以保证DP值为正的地方就是能走的地方。然后ans取max即可。

四个方向的DP转移方程式如下:

\[\left\{\begin{matrix}
dir=1&dp[i][j]=max\{dp[k][j]+k\}-i & i\leqslant k\leqslant min\{n,dmax+i\} \\
dir=2&dp[i][j]=max\{dp[k][j]-k\}+i & i-min\{i,dmax\}\leqslant k\leqslant i \\
dir=3&dp[i][j]=max\{dp[i][k]+k\}-j & j\leqslant k\leqslant min\{n,dmax+j\} \\
dir=4&dp[i][j]=max\{dp[k][j]-k\}+j & j-min\{j,dmax\}\leqslant k\leqslant j
\end{matrix}\right.
\]

用单调队列优化后即可得到 \(O(nm)\) 的转移复杂度。总复杂度为 \(O(knm)\)。

#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

神题好吧。一般单调队列是固定左右端点移动中间的 \(k\) 值,这个是固定中间的 \(k\) 值不断扩展左右端点。

定义状态 dp[j][i] 表示从第 \(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;
}

[学习笔记] 单调队列优化DP - DP的更多相关文章

  1. 算法笔记--单调队列优化dp

    单调队列:队列中元素单调递增或递减,可以用双端队列实现(deque),队列的前面和后面都可以入队出队. 单调队列优化dp: 问题引入: dp[i] = min( a[j] ) ,i-m < j ...

  2. 【NOIP2017】跳房子 题解(单调队列优化线性DP)

    前言:把鸽了1个月的博客补上 ----------------- 题目链接 题目大意:机器人的灵敏性为$d$.每次可以花费$g$个金币来改造机器人,那么机器人向右跳的范围为$[min(d-g,1),m ...

  3. 洛谷p1725 露琪诺 单调队列优化的DP

    #include <iostream> #include <cstdio> #include <cstring> using namespace std; int ...

  4. BestCoder Round #89 02单调队列优化dp

    1.BestCoder Round #89 2.总结:4个题,只能做A.B,全都靠hack上分.. 01  HDU 5944   水 1.题意:一个字符串,求有多少组字符y,r,x的下标能组成等比数列 ...

  5. 单调队列以及单调队列优化DP

    单调队列定义: 其实单调队列就是一种队列内的元素有单调性的队列,因为其单调性所以经常会被用来维护区间最值或者降低DP的维数已达到降维来减少空间及时间的目的. 单调队列的一般应用: 1.维护区间最值 2 ...

  6. bzoj1855: [Scoi2010]股票交易 单调队列优化dp ||HDU 3401

    这道题就是典型的单调队列优化dp了 很明显状态转移的方式有三种 1.前一天不买不卖: dp[i][j]=max(dp[i-1][j],dp[i][j]) 2.前i-W-1天买进一些股: dp[i][j ...

  7. 【笔记篇】单调队列优化dp学习笔记&&luogu2569_bzoj1855股票交♂易

    DP颂 DP之神 圣洁美丽 算法光芒照大地 我们怀着 崇高敬意 跪倒在DP神殿里 你的复杂 能让蒟蒻 试图入门却放弃 在你光辉 照耀下面 AC真心不容易 dp大概是最经久不衰 亘古不化的算法了吧. 而 ...

  8. 「学习笔记」单调队列优化dp

    目录 算法 例题 最大子段和 题意 思路 代码 修剪草坪 题意 思路 代码 瑰丽华尔兹 题意 思路 代码 股票交易 题意 思路 代码 算法 使用单调队列优化dp 废话 对与一些dp的转移方程,我们可以 ...

  9. 【刷题笔记】DP优化-单调队列优化

    单调队列优化 眼界极窄的ZZ之前甚至不会单调队列--(好丢人啊) 单调队列优化的常见情景: 转移可以转化成只需要确定一个维度,而且这个维度的取值范围在某个区间里 修剪草坪 这个题学长讲的好像是另外一个 ...

  10. POJ - 1821 单调队列优化DP + 部分笔记

    题意:n个墙壁m个粉刷匠,每个墙壁至多能被刷一次,每个粉刷匠要么不刷,要么就粉刷包含第Si块的长度不超过Li的连续墙壁(中间可不刷),每一块被刷的墙壁都可获得Pi的利润,求最大利润 避免重复粉刷: 首 ...

随机推荐

  1. LLM学习笔记

    1. 评估榜单 1.1. C-Eval C-Eval 是一个全面的中文基础模型评估套件.它包含了13948个多项选择题,涵盖了52个不同的学科和四个难度级别. https://cevalbenchma ...

  2. 使用AWS Glue进行 ETL 工作

    数据湖 数据湖的产生是为了存储各种各样原始数据的大型仓库.这些数据根据需求,进行存取.处理.分析等.对于存储部分来说,开源版本常见的就是 hdfs.而各大云厂商也提供了各自的存储服务,如 Amazon ...

  3. 更难、更好、更快、更强:LLM Leaderboard v2 现已发布

    摘要 评估和比较大语言模型 (LLMs) 是一项艰巨的任务.我们 RLHF 团队在一年前就意识到了这一点,当时他们试图复现和比较多个已发布模型的结果.这几乎是不可能完成的任务:论文或营销发布中的得分缺 ...

  4. mac svn管理工具

    App Store中搜索snailsvn 分付费(98元)和免费试用

  5. Servlet3.0+SpringBoot2.X注解Listener常用监听器

    监听器:应用启动监听器,会话监听器,请求监听器 作用: ServletContextListener 应用启动监听 HttpSessionLisener 会话监听 ServletRequestList ...

  6. Mac制作U盘启动项

    导读 鄙人刚买回来的电脑,自带系统版本:10.14.5(19款的),有一天,提示系统升级,升到了10.15.4,从此落下了后遗症,mac系统密码输入完之后,读条读到2/3的时候,会黑屏闪一下,百思不得 ...

  7. 全网最适合入门的面向对象编程教程:02 类和对象的Python实现-使用Python创建类

    全网最适合入门的面向对象编程教程:02 类和对象的 Python 实现-使用 Python 创建类 摘要 本文主要介绍了串口通信协议的基本概念.串口通信的基本流程.如何使用 Python 语言创建一个 ...

  8. 载均衡技术全解析:Pulsar 分布式系统的最佳实践

    背景 Pulsar 有提供一个查询 Broker 负载的接口: /** * Get load for this broker. * * @return * @throws PulsarAdminExc ...

  9. 使用ES6中Class实现手写PromiseA+,完美通过官方872条用例

    目录 Promise出现的原因 myPromise的实现要点 myPromise的实现 myPromise - 实现简单的同步 myPromise - 增加异步功能 myPromise - 链式调用( ...

  10. [oeasy]python0132_[趣味拓展]emoji_表情符号_抽象话_由来_流汗黄豆

    emoji表情符号 回忆上次内容 上次了解了unicode 和 utf-8 unicode是字符集 utf-8是一种可变长度的编码方式 utf-8是实现unicode的存储和传输的现实的方式   ​ ...