单调队列及单调队列优化DP

首先是单调队列: 

其实单调队列就是一种队列内的元素有单调性(单调递增或者单调递减)的队列,答案(也就是最优解)就存在队首,而队尾则是最后进队的元素。因为其单调性所以经常会被用来维护区间最值或者降低DP的维数已达到降维来减少空间及时间的目的。

类似于滑动窗口等,单调队列具有时序性的储存区间最大值或区间最小值,以下为例题:
P1886 滑动窗口 /【模板】单调队列 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

P1440 求m区间内的最小值 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

上题均可用滑动窗口模板解决:

#include<bits/stdc++.h> using namespace std; const int N=2e6+10; int n,m,hh,tt,a[N],q[N],k; int main() { cin>>n>>k; for(int i=1;i<=n;i++) cin>>a[i]; hh=1,tt=0; for(int i=1;i<=n;i++){ while(hh<=tt&&q[hh]<i-k+1) hh++; while(hh<=tt&&a[q[tt]]>=a[i]) tt--; q[++tt]=i; if(i>=k) cout<<a[q[hh]]<<' '; } cout<<endl; hh=1,tt=0; for(int i=1;i<=n;i++){ while(hh<=tt&&q[hh]<i-k+1) hh++; while(hh<=tt&&a[q[tt]]<a[i]) tt--; q[++tt]=i; if(i>=k) cout<<a[q[hh]]<<' '; } return 0; }

对于单调队列优化DP,在状态转移的时候会发生我们需要转移到一个区间的某一个点上,而在最佳答案的情景下这个点一般为最大值或者最小值,考虑遍历区间复杂度较高,可采用求区间最值方法来确定一个点的位置即可:
P1725 琪露诺 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

对于该题,我们从位置 0 开始,然后在位置 i 时,无非是是从区间 [ir,il] 转移而来,故我们使用单调队列储存 dp 状态在这个区间的最大值即可,区间最大值加上现在位置的贡献即为这个位置状态的贡献,如果从某个位置开始可以直接跳走,即 i+r>n 则从此时开始更新迭代答案即可,这里再解释以下为什么是 il 入队,因为我们向前走一步,即 ii+1, 我们的区间窗口也要滑动,此时新进来的数就是 il

#include<bits/stdc++.h> #define int long long using namespace std; const int N=2e6+10; int n,l,r,q[N],hh=1,tt,dp[N],a[N],res=-1e18; signed main(){ cin>>n>>l>>r; for(int i=1;i<=2*n;i++) dp[i]=-1e18; for(int i=0;i<=n;i++) cin>>a[i]; for(int i=l;i<=n;i++){ while(hh<=tt&&q[hh]<i-r) hh++; while(hh<=tt&&dp[q[tt]]<dp[i-l]) tt--; q[++tt]=i-l,dp[i]=dp[q[hh]]+a[i]; if(i+r>n) res=max(res,dp[i]); } cout<<res; }

P3957 [NOIP2017 普及组] 跳房子 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

题目要求最小金钱数目,则可以考虑用二分,因为我们可以具有区间灵活度,花费金钱越大灵活度越大,所以最终的答案一定是递增的,故可以使用二分答案

接下来是 check 函数,考虑使用单调队列DP,由于每次进行的步数都不一样,所以此时进行状态转移即可得到最优解,首先确定每次跳跃的区间左端点和右端点,然后进行遍历,设置一个变量 j,表示最后一个加入队伍的编号,对于 i 之前的距离,如果 ij 之间相差的距离满足跳跃区间,那么可以考虑是否加入队列,此时单调队列更新完成,更新 dp[i] 即可,由于游戏随时可以停止,所以要时刻判断

#include<bits/stdc++.h> #define int long long using namespace std; const int N=2e6+10; pair<int,int>robot[N]; int n,d,k,dp[N],q[N],sum; bool check(int g){ int l,r,hh=1,tt=0,j=0; if(g>=d) l=1,r=d+g; else l=d-g,r=d+g; for(int i=1;i<=n;i++) dp[i]=-2e9,q[i]=0; dp[0]=0,q[0]=0; for(int i=1;i<=n;i++){ while(robot[i].first-robot[j].first>=l&&j<i){ if(dp[j]>-2e9){ while(hh<=tt&&dp[q[tt]]<=dp[j]) tt--; q[++tt]=j; } j++; } while(hh<=tt&&robot[i].first-robot[q[hh]].first>r) hh++; if(hh<=tt) dp[i]=dp[q[hh]]+robot[i].second; if(dp[i]>=k) return true; } return false; } signed main(){ cin>>n>>d>>k; for(int i=1;i<=n;i++){ int a,b; cin>>a>>b; robot[i]={a,b}; } int l=1,r=1e9; while(l<r){ int mid=l+r>>1; if(check(mid)) r=mid; else l=mid+1; } cout<<(r==1e9?-1:r); }

 单调队列优化多重背包:

若用F[i][j]表示对容量为j的背包,处理完前i种物品后,背包内物品可达到的最大总价值,并记m[i]=min(n[i],j/v[i])。放入背包的第i种物品的数目可以是:012,可得:

F[i][j]=maxF[i1][jkv[i]]+kw[i](0<=k<=m[i])

 

如何在O(1)时间内求出F[i][j]呢?

先看一个例子:取m[i]=2,v[i]=v,w[i]=w,V>9v

并假设 f(j) = F[i - 1][j],观察公式右边要求最大值的几项:

j=6v:f(6v)f(5v)+wf(4v)+2w 这三个中的最大值

j=5v:f(5v)f(4v)+wf(3v)+2w 这三个中的最大值

j=4v:f(4v)f(3v)+wf(2v)+2w 这三个中的最大值

显然,公式㈠右边求最大值的几项随j值改变而改变,但如果将j = 6*v时,每项减去6*w,j=5*v时,每项减去5*w,j=4*v时,每项减去4*w,就得到:

j=6v:f(6v)6wf(5v)5wf(4v)4w 这三个中的最大值

j=5v:f(5v)5wf(4v)4wf(3v)3w 这三个中的最大值

j=4v:f(4v)4wf(3v)3wf(2v)2w 这三个中的最大值

很明显,要求最大值的那些项,有很多重复。

 

根据这个思路,可以对原来的公式进行如下调整:

假设d=v[i]a=j/db=j,即 j=ad+b,代入公式㈠,并用k替换ak得:

F[i][j]=maxF[i1][b+kd]kw[i]+aw[i](am[i]<=k<=a)

 

F[i1][y]y=bb+db+2db+3db+4db+5db+6dj)

F[i][j]就是求j的前面m[i]+1个数对应的F[i1][b+kd]kw[i]的最大值,加上aw[i],如果将F[i][j]前面所有的F[i1][b+kd]kw放入到一个队列,那么,F[i][j]就是求这个队列最大长度为m[i]+1时,队列中元素的最大值,加上aw[i]。因而原问题可以转化为:O(1)时间内求一个队列的最大值。

 

该问题可以这样解决:

① 用另一个队列B记录指定队列的最大值(或者记录最大值的地址),并通过下面两个操作保证队列B的第一个元素(或其所指向的元素)一定是指定队列的当前最大值。

② 当指定队列有元素M进入时,删除队列B中的比M小的(或队列B中所指向的元素小等于M的)所有元素,并将元素M(或其地址)存入队列B。

③ 当指定队列有元素M离开时,队列B中的第一个元素若与M相等(或队列B第一个元素的地址与M相等),则队列B的第一个元素也离队。

 

经过上述处理,可以保证队列B中的第一个元素(或其指向的元素)一定是所指定队列所有元素的最大值。显然队列B的元素(或其所指向的元素)是单调递减的,这应该就是《背包九讲》中的提到的“单调队列”吧,初看的时候被这个概念弄得稀里糊涂,网上的资料提到“维护队列的最大值”,刚开始还以为是维护这个单调队列的最大值,对其采用的算法,越看越糊涂。其实,只要明白用一个“辅助队列”,求另一个队列的最值,那么具体的算法,和该“辅助队列”的性质(单调变化),都很容易推导出来。

 

在多重背包问题中,所有要进入队列的元素个数的上限值是已知的,可以直接用一个大数组模拟队列。

//“多重背包”通用模板 const int MAX_V = 100004; //v、n、w:当前所处理的这类物品的体积、个数、价值 //V:背包体积, MAX_V:背包的体积上限值 //f[i]:体积为i的背包装前几种物品,能达到的价值上限。 inline void pack(int f[], int V, int v, int n, int w) { if (n == 0 || v == 0) return; if (n == 1) { //01背包 for (int i = V; i >= v; --i) if (f[i] < f[i - v] + w) f[i] = f[i - v] + w; return; } if (n * v >= V - v + 1) { //完全背包(n >= V / v) for (int i = v; i <= V; ++i) if (f[i] < f[i - v] + w) f[i] = f[i - v] + w; return; } int va[MAX_V], vb[MAX_V]; //va/vb: 主/辅助队列 for (int j = 0; j < v; ++j) { //多重背包 int *pb = va, *pe = va - 1; //pb/pe分别指向队列首/末元素 int *qb = vb, *qe = vb - 1; //qb/qe分别指向辅助队列首/末元素 for (int k = j, i = 0; k <= V; k += v, ++i) { if (pe == pb + n) { //若队列大小达到指定值,第一个元素X出队。 if (*pb == *qb) ++qb; //若辅助队列第一个元素等于X,该元素也出队。 ++pb; } int tt = f[k] - i * w; *++pe = tt; //元素X进队 //删除辅助队列所有小于X的元素,qb到qe单调递减,也可以用二分法 while (qe >= qb && *qe < tt) --qe; *++qe = tt; //元素X也存放入辅助队列 f[k] = *qb + i * w; //辅助队列首元素恒为指定队列所有元素的最大值 } } }

 


__EOF__

本文作者Sakurajimamai
本文链接https://www.cnblogs.com/o-Sakurajimamai-o/p/18009957.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   o-Sakurajimamai-o  阅读(15)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
-- --
点击右上角即可分享
微信分享提示