单调队列和斜率优化 dp
本文参考了 OI Wiki 和 《算法竞赛进阶指南》。
引入:单调队列
定义
单调队列是一种可以在两头弹出元素,只在队尾插入元素的双端队列。
单调队列的元素满足某种单调性。在插入新的元素前,需要去掉原来的元素中不符合单调性的元素,然后加上新的元素。故而其解决的问题需要有某种单调性,不满足单调性的元素必在将来不能作为可以计入答案的元素。这样导致每一次在队头总会有满足最值性质的元素。
例题:滑动窗口
代码
点此查看代码
#include <bits/stdc++.h> using namespace std; const int maxn=1000010; int n,k,i,l,r; int a[maxn],q[maxn]; int main(){ scanf("%d%d",&n,&k); for(i=1;i<=n;++i) scanf("%d",a+i); l=q[1]=1; for(i=1;i<k;++i){ while(l<=r&&a[i]<=a[q[r]]) --r; q[++r]=i; } for(i=k;i<=n;++i){ if(i-q[l]==k) ++l; while(l<=r&&a[i]<=a[q[r]]) --r; q[++r]=i;printf("%d ",a[q[l]]); } l=1;r=0;putchar('\n'); for(i=1;i<k;++i){ while(l<=r&&a[i]>=a[q[r]]) --r; q[++r]=i; } for(i=k;i<=n;++i){ if(i-q[l]==k) ++l; while(l<=r&&a[i]>=a[q[r]]) --r; q[++r]=i;printf("%d ",a[q[l]]); } }
单调队列优化 dp
适用条件
形如 (其中若 与 有关,则有 )。
若 单调不降,则 dp 式的计算中,可以处理一个 单调递增的单调队列。在计算 时,先在队列上二分找到第一个不小于 的元素,其对应的值即为 的最优决策。在将 入队前将 的 弹出队列,再将 从小到大入队。
由于之后 ,则必有 ,而始终有 ,故而 在 能作为某个合法决策时总是可以替代 。
由于每个元素保证只会入队/出队一次,则最后转移的总时间复杂度是 的。
如果不能保证 单调不降,则此题型可以用线段树优化 dp 解决。
当然,如果同时保证 单调不降,则可以在转移 之前排除过时队头,可以做到转移的总时间复杂度是 的。
例题1:CF372C Watching Fireworks is Fun
题意
一个城镇有 个区域,从左到右从 编号为 ,每个区域之间距离 个单位距离。
节日中有 个烟花要放,给定放的地点 ,时间 ,如果你当时在区域 ,那么你可以获得 的开心值。
你每个单位时间可以移动不超过 个单位距离。
你的初始位置是任意的(初始时刻为 ),求你通过移动能获取到的最大的开心值。
。
解法
设 为在烟花 燃放时处于位置 的最大快乐值。
考虑 能够从哪些 值转移而来。显然需要 。故而转移有 。
代码
点此查看代码
#include <bits/stdc++.h> using namespace std; #define ll long long const int maxm=310; const int maxn=150010; int n,m,d,l,r,i,j; int q[maxn]; ll dp[2][maxn],*dp1=dp[0],*dp2=dp[1]; ll a,b,t,lst=1,del,det; template<typename T>inline T Abs(const T &a){return a>0?a:-a;} int main(){ scanf("%d%d%d",&n,&m,&d); for(i=1;i<=m;++i){ scanf("%lld%lld%lld",&a,&b,&t); del=(t-lst)*d; l=r=q[1]=1;det=b-a; for(j=1;j<=n;++j){ if(j<=a) ++det;else --det; while(l<=r&&q[l]+del<j) ++l; dp2[j]=dp1[q[l]]+det; while(l<=r&&dp1[j+1]>dp1[q[r]]) --r; q[++r]=j+1; } l=r=1;q[1]=n; det=b-Abs(n+1-a); for(j=n;j;--j){ if(j<a) --det;else ++det; while(l<=r&&q[l]-del>j) ++l; dp2[j]=max(dp2[j],dp1[q[l]]+det); while(l<=r&&dp1[j-1]>dp1[q[r]]) --r; q[++r]=j-1; } lst=t; swap(dp1,dp2); } a=-11451419198101926; for(i=1;i<=n;++i) a=max(a,dp1[i]); printf("%lld\n",a); return 0; }
例题2:Fence
题意
有 块木板从左到右排成一行,有 个工匠对这些木板进行粉刷,每块木板至多被粉刷一次。
第 个木匠要么不粉刷,要么粉刷包含木板 的,长度不超过 的连续的一段木板,每粉刷一块可以得到 的报酬。
不同工匠的 不同。
请问如何安排能使工匠们获得的总报酬最多。
。
解法
考虑维护 表示考虑第 位工匠,和第 块木板时的最大收益。
考虑 可以从哪些 值转移而来。
- 第 位工匠可以不粉刷木板,此时 。
- 第 块木板可以不粉刷,此时 。
- 第 位工匠粉刷包括第 的木板,此时 。
此时可以直接预处理 的决策集合,对于每一个 作转移即可。
注意需要对所有工匠按 升序排序,可能有某位工匠在中间新粉刷木板的情况。
代码
点此查看代码
#include <bits/stdc++.h> using namespace std; const int maxn=16100; const int maxm=110; int dp[2][maxn],v[maxn],q[maxn]; int *f=dp[0],*g=dp[1]; int n,m,i,j,b,l,p,s,lt,rt; struct gj{ int l,p,s; inline bool operator <(const gj &a)const{return s<a.s;} }N[maxm]; int main(){ scanf("%d%d",&n,&m); for(i=1;i<=m;++i) scanf("%d%d%d",&N[i].l,&N[i].p,&N[i].s); sort(N+1,N+m+1); for(i=1;i<=m;++i){ l=N[i].l,p=N[i].p,s=N[i].s; for(j=1;j<=n;++j) v[j]=g[j]-j*p; swap(f,g); for(j=1;j<s;++j) g[j]=max(g[j-1],f[j]); lt=1;rt=0; for(j=max(s-l,0);j<s;++j){ while(rt&&v[q[rt]]<=v[j]) --rt; q[++rt]=j; } b=min(s+l-1,n); for(j=s;j<=b;++j){ g[j]=max(g[j-1],f[j]); if(q[lt]<j-l) ++lt; g[j]=max(g[j],j*p+v[q[lt]]); } for(j=b+1;j<=n;++j) g[j]=max(g[j-1],f[j]); } printf("%d",g[n]); }
例题3:Cut the Sequence
题意
给定一个长度为 的序列 ,要求把该序列分成若干段,在满足“每段中所有数的和”不超过 的前提下,让“每段中所有数的最大值”之和最小。
试计算这个最小值。
。
解法
记 表示对 进行划分,则转移有 ,其中 。考虑由 递推到 。
考虑将 拆开,判断两个决策之间的优劣。下面只比较 和 决策而非其他决策,其中 。
首先显然有 单调不降,故而有 。如果 (也就是 ,即为 ),则 ,则 一定优于 。故而在 每次查找 的可能最优决策前,先把 的 移出单调队列。如果之前已经将 的 移出队列,则这次需要把 的 移出队列。
综上,可以维护一个 递减的单调队列 ,在将 入队前移出所有非法 ,在队列中保存可能的最优解,再将 入队。同时, 即为 。(可以用反证法证明此结论。)
至于找出 (即 ),可以同时维护一个 std::multiset
,以 为键值,维护队列中所有元素对应的 。(同时可以使用优先队列 + 懒惰删除)
注意若令 ,则 初值为 (因为 始终为一个合法且可能的最优决策,处理队列为空的情况,此时显然 ,如此则需要在将 入队前将 移出队列)。
代码
点此查看代码
#include <bits/stdc++.h> using namespace std; const int maxn=100010; int n,i,l,r,u; int a[maxn],q[maxn]; long long m,dp[maxn],pre[maxn]; multiset<long long> s; int main(){ scanf("%d%lld",&n,&m); l=1;s.insert(11451419198101926LL); for(i=1;i<=n;++i){ scanf("%d",a+i); pre[i]=pre[i-1]+a[i]; if(a[i]>m){ printf("-1"); return 0; } while(pre[i]-pre[u]>m) ++u; while(l<=r&&q[l]<=u){ if(l<r) s.erase(s.find(dp[q[l]]+a[q[l+1]])); ++l; } while(l<=r&&a[i]>=a[q[r]]){ if(l<r) s.erase(s.find(dp[q[r-1]]+a[q[r]])); --r; } if(l<=r) s.insert(dp[q[r]]+a[i]); q[++r]=i;dp[i]=min(*s.begin(),dp[u]+a[q[l]]); } printf("%lld",dp[n]); }
例题4:多重背包
题意
有一个容积为 的背包,同时有 种物品,第 种物品有 个,每个的体积为 ,价值为 。求可以得到的最大价值。。
解法
这个题似乎没有人卡二进制优化,反正二进制优化就比单调队列优化慢了一倍 可能是常数的原因
考虑把每一种物品分开考虑。
设 为考虑完第 种物品后,体积之和为 的物品价值之和最多是多少。
转移方程如下:
此时如果令 ,只考虑 ,则这个方程中的 可以看成是上述集合内的一段连续段。此时对于 ,可以把原式看成是
其中 。
代码
点此查看代码
//二进制 #include <bits/stdc++.h> using namespace std; int C[10010],V[10010]; int dp[40010],mx; int n,w,c,v,m,tmp,j=0,top; int main(){ scanf("%d%d",&n,&w); for(int i=0;i<n;++i){ scanf("%d%d%d",&c,&v,&m); while(m>=(1<<j)){ C[top]=c*(1<<j); V[top++]=v*(1<<j); m-=(1<<(j++)); } if(m!=(j=0)){ C[top]=c*m; V[top++]=v*m; } } for(int i=0;i<top;++i){ for(int j=w;j>=V[i];--j){ tmp=dp[j-V[i]]+C[i]; if(tmp>dp[j]) dp[j]=tmp; if(mx<dp[j]) mx=dp[j]; } } printf("%d",mx); return 0; }
点此查看代码
//单调队列 #include <bits/stdc++.h> using namespace std; const int maxn=40010; int n,v,w,c,l,r,i,j,k,a,b; int q[maxn],dp[2][maxn],val[maxn]; int *N=dp[0],*M=dp[1]; int main(){ scanf("%d%d",&n,&a); while(n--){ scanf("%d%d%d",&w,&v,&c); for(i=0;i<v;++i){ l=1;r=0;k=0;b=i-v*c; for(j=i;j<=a;j+=v) val[j]=N[j]-(k++)*w; k=0; for(j=i;j<=a;j+=v){ if(q[l]<b) ++l; while(l<=r&&val[q[r]]<=val[j]) --r; q[++r]=j;M[j]=val[q[l]]+w*(k++);b+=v; } } swap(N,M); } b=0; for(j=0;j<=a;++j) b=max(b,N[j]); printf("%d",b); }
引入:斜率(请自学初中数学)
引入:函数的凹凸性
定义一个定义在 上的函数 是凸函数当且仅当其二阶差分恒非负(形式化即为:),其是凹函数当且仅当其二阶差分恒非正(形式化即为:)。
斜率优化 dp
适用条件
形如 的式子(其中条件和单调队列优化 dp 相似)。
做法
可以把 看作一个点,把 的这些点作为一个点集,这样问题即转化成了:对每一个这样的点作一条斜率恒为 的直线,这些直线在纵轴上的截距的最小值是多少。
对于任意两点 ,此时若有 ,且令 (即为两点之间的斜率),则 优于 当且仅当 。
证明:若 ,则 ,而 ,故而 。
同时,若三个点 形成了一个非下凸曲线,也就是 ,则若 同时优于 ,则必有 且 ,而这两者一定不会同时成立,可以直接在决策集合内删去 。
故而最后可能形成最优解的点集一定形成一个下凸壳的形状,对应在单调队列中必有相邻两点的斜率单调递减。同时找到一个最优点等效于用一条斜率为 的直线截这个下凸壳,对应在单调队列中找出一个点 满足 。可以直接在队列上二分,找到对应的点。
若 函数单调不增,则可以直接删除斜率过小的点,可以去掉一个 ;若 函数单调,则可以直接顺次加点;否则需要在任意位置加入点,需要用到平衡树维护凸壳。
例题1:玩具装箱
题意
有 个玩具,第 个玩具价值为 。要求将这 个玩具排成一排,分成若干段。对于一段 ,它的代价为 。其中 是一个常量,求分段的最小代价。。
解法
设 为考虑前 个物品的最小代价,则转移方程有
其中 。
此时令 为 , 为 , 为 , 为 ,则方程转为 。可以直接使用上述方式即可。
细节见代码。
代码
点此查看代码
#include <bits/stdc++.h> using namespace std; const int maxn=50010; int n,L,l,r,c,i,j; int q[maxn]; long long k,dp[maxn],pre[maxn],xt[maxn],yt[maxn]; long long fz[maxn],fm[maxn]; __int128_t t1,t2; int main(){ scanf("%d%d%d",&n,&L,&c); dp[1]=1LL*(c-L)*(c-L); fm[0]=xt[1]=1+c; fz[0]=yt[1]=dp[1]+xt[1]*xt[1]; q[1]=r=1;pre[1]=c;//此处 0 点需要入队,因为这个点可能是其他点转移的最优决策 for(i=2;i<=n;++i){ scanf("%d",&c); pre[i]=pre[i-1]+c; xt[i]=i+pre[i]; k=(xt[i]-L-1)*2; while(l<r){ t1=(__int128_t)fm[q[l]]*k; if(t1>fz[q[l]]) ++l; else break; } j=q[l]; dp[i]=yt[j]-k*xt[j]+((k>>1)*(k>>1)); yt[i]=dp[i]+xt[i]*xt[i]; while(l<r){ t1=(yt[q[r]]-yt[q[r-1]])*(xt[i]-xt[q[r]]); t2=(xt[q[r]]-xt[q[r-1]])*(yt[i]-yt[q[r]]); if(t1>=t2) --r; else break; } fz[q[r]]=yt[i]-yt[q[r]]; fm[q[r]]=xt[i]-xt[q[r]]; fz[i]=11451419198101926; q[++r]=i; } printf("%lld",dp[n]); return 0; }
例题2:玩具装箱 · 改
题意
有 个玩具,第 个玩具价值为 。要求将这 个玩具排成一排,分成若干段。对于一段 ,它的代价为 。其中 是一个常量,求分段的最小代价。。
转移方程同上。但是新加入的决策的对应坐标横坐标不单调。
考虑将现有的凸壳按照新的 拆开成两部分。然后如果对于新的点,存在两个原凸壳上的点与之形成了一个上凸形,则这个点应该剔除,否则在两部分凸壳两侧删除掉若干个成为上凸点的点即可。不用判断横坐标是否相同的情况。 可以用平衡树维护。
p.s. 用 std::set
也可以实现这样的操作。
解法 1 代码
点此查看代码
#include <bits/stdc++.h> using namespace std; const int maxn=100010; const long long inf=1145141919810192608; mt19937 Rand(time(0)); int n,l,i,j,k,c,xp,yp,px,pt,pr; int tot,root; int pre[maxn]; long long dp[maxn],xt[maxn],yt[maxn]; struct node{ int idx,ls,rs; unsigned key; long long fz,fm; }tr[maxn]; #define idx(p) tr[p].idx #define ls(p) tr[p].ls #define rs(p) tr[p].rs #define key(p) tr[p].key #define fz(p) tr[p].fz #define fm(p) tr[p].fm #define xt(p) xt[idx(p)] #define yt(p) yt[idx(p)] void vSplit(const int p,int &x,int &y){ if(!p){ x=y=0; return; } if(xt[idx(p)]<xt[i]){ x=p; vSplit(rs(p),rs(p),y); } else{ y=p; vSplit(ls(p),x,ls(p)); } } int Merge(const int x,const int y){ if(!(x&&y)) return x|y; if(key(x)<key(y)){ rs(x)=Merge(rs(x),y); return x; } else{ ls(y)=Merge(x,ls(y)); return y; } } int main(){ scanf("%d%d",&n,&l); root=tot=1; key(1)=Rand(); fz(1)=inf; for(i=1;i<=n;++i){ fz(i+1)=inf; scanf("%d",&c); pre[i]=pre[i-1]+c; xt[i]=i+pre[i]; k=(xt[i]-l-1)<<1; pt=root;j=-1; for(;;){ if((fm(pt)*k>fz(pt)&&fm(pt)>=0)|| (fm(pt)*k<fz(pt)&&fm(pt)<0)){ if(!rs(pt)) break; pt=rs(pt); } else{ j=pt; if(!ls(pt)) break; pt=ls(pt); } } if(j<0) j=pt; dp[i]=yt(j)-k*xt(j)+(1LL*(k>>1)*(k>>1)); yt[i]=dp[i]+xt[i]*xt[i]; vSplit(root,xp,yp); pr=0;pt=yp;px=xp; while(ls(pt)) pr=pt,pt=ls(pt); while(rs(px)) px=rs(px); if((pt&&px)&&((yt[i]-yt(px))*(xt(pt)-xt[i])>= (yt(pt)-yt[i])*(xt[i]-xt(px)))) goto end; key(++tot)=Rand(); idx(tot)=i; while(yp){ pt=yp;pr=0; while(ls(pt)) pr=pt,pt=ls(pt); if(!(rs(pt)||pr)) break; if(((fz(pt)*(xt(pt)-xt[i])<= fm(pt)*(yt(pt)-yt[i]))&& (fm(pt)>0))|| ((fz(pt)*(xt(pt)-xt[i])>= fm(pt)*(yt(pt)-yt[i]))&& (fm(pt)<0))){ if(pr) ls(pr)=rs(pt); else{ yp=rs(yp); rs(pt)=0; } } else break; } if(yp){ fz(tot)=yt(pt)-yt[i]; fm(tot)=xt(pt)-xt[i]; yp=Merge(tot,yp); } else yp=tot; pt=0; while(xp){ pt=xp;pr=0; while(rs(pt)) pr=pt,pt=rs(pt); if(ls(pt)){ px=ls(pt); while(rs(px)) px=rs(px); } else px=pr; if(!px) break; if(((fz(px)*(xt[i]-xt(pt))>= fm(px)*(yt[i]-yt(pt)))&& (fm(px)>0))|| ((fz(px)*(xt[i]-xt(pt))<= fm(px)*(yt[i]-yt(pt)))&& (fm(px)<0))){ if(pr) rs(pr)=ls(pt); else{ xp=ls(xp); ls(pt)=0; } } else break; } if(pt){ fz(pt)=yt[i]-yt(pt); fm(pt)=xt[i]-xt(pt); } end:root=Merge(xp,yp); } printf("%lld",dp[n]); }
解法 2
把上面的转移方程 换成 的形式,则问题等效于每次插入一条直线并且求所有直线在某个 值处的最值。这个问题可以使用 李超线段树 解决,并且码长、常数等方面显然更优。很多类似内容均可以使用这两种方式解决。
P4655 [CEOI2017] Building Bridges 解法 2 代码
点此查看代码
#include <bits/stdc++.h> using namespace std; const int maxn=100010; const int INF=1000000; const int maxx=1000010; #define ll long long int n,i; ll dp,w[maxn],h[maxn]; struct line{ int k; ll b; bool f; inline ll val(int x){return (1LL*x*k)+b;} }tr[maxx<<2]; #define ls(p) p<<1 #define rs(p) p<<1|1 void Insert(int p,int l,int r,line c){ if(!tr[p].f){ tr[p]=c; return; } int m=(l+r)>>1; if(c.val(m)<tr[p].val(m)) swap(c,tr[p]); if(l==r) return; if(c.val(l)<tr[p].val(l)) Insert(ls(p),l,m,c); else if(c.val(r)<tr[p].val(r)) Insert(rs(p),m+1,r,c); } void Query(int p,int l,int r,int x){ if(!tr[p].f) return; dp=min(dp,tr[p].val(x)); if(l==r) return; int m=(l+r)>>1; if(x<=m) Query(ls(p),l,m,x); else Query(rs(p),m+1,r,x); } int main(){ scanf("%d",&n); for(i=1;i<=n;++i) scanf("%lld",h+i); scanf("%lld",w+1); Insert(1,0,INF,{(int)(-2*h[1]),h[1]*h[1]-w[1],1}); for(i=2;i<=n;++i){ scanf("%lld",w+i); w[i]+=w[i-1]; dp=1e18; Query(1,0,INF,h[i]); dp+=w[i-1]+h[i]*h[i]; Insert(1,0,INF,{(int)(-2*h[i]),dp+h[i]*h[i]-w[i],1}); } printf("%lld",dp); return 0; }
本文来自博客园,作者:Fran-Cen,转载请注明原文链接:https://www.cnblogs.com/Fran-CENSORED-Cwoi/p/16753906.html
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战