DP优化笔记
DP 的效率取决于
- 状态总数优化:相当于搜索的剪枝,去除无用状态;使用降维,设计 DP 状态时尽量使用低维的 DP。
- 决策数量优化:即状态转移方程的优化,减少决策数量,如斜率优化,四边形不等式优化。
- 状态转移计算量优化:如用预处理减少递推时间,用 Hash 表,线段树,树状数组等减少枚举时间。
一. 一般优化
几种基础的优化方式。
1. 倍增优化
动态规划是多阶段递推,可以使用倍增法将阶段性的线性增长加速为成倍增长。经典应用有 ST表,背包中的二进制拆分等。
2. 数据结构优化
如果题目与简单的区间操作有关,如区间查询,区间修改等,可以用线段树或者树状数组进行优化。把区间操作的复杂度优化到
2.1 树状数组优化
例题 P3287
首先注意到一个性质:对于在
-
如果
,会导致 后面的数相对变小,不利于形成更长的单调不降序列。 -
如果
,至少不会使单调不降序列变短。
令
这是一个二维区间问题,可以使用二维树状数组进行优化。
#include<bits/stdc++.h> using namespace std; const int N=10009; int n,k,ans,a[N],f[N][509],bit[509][5509]; int lsb(int x){return x&(-x);} void update(int x,int y,int z){ for(int i=x;i<=k+1;i+=lsb(i)){ for(int j=y;j<=5500;j+=lsb(j)) bit[i][j]=max(bit[i][j],z); } } int query(int x,int y){ int res=0; for(int i=x;i;i-=lsb(i)){ for(int j=y;j;j-=lsb(j)) res=max(res,bit[i][j]); } return res; } int main(){ ios::sync_with_stdio(false); cin>>n>>k; for(int i=1;i<=n;i++) cin>>a[i]; for(int i=1;i<=n;i++){ for(int j=k;j>=0;j--){ f[i][j]=query(j+1,a[i]+j)+1; ans=max(ans,f[i][j]); update(j+1,a[i]+j,f[i][j]); } } cout<<ans; return 0; }
2.2. 线段树优化
例题 P2605
定义
其中
如果直接计算的话,需要
-
滚动数组:发现
只与 有关,可以用滚动数组优化 ,将复杂度降为 ,优化后的状态转移方程: -
区间操作的优化:方程中的
数组计算 内的赔偿费用,是一个区间求和问题,用线段树优化。
#include<bits/stdc++.h> using namespace std; typedef long long ll; const int N=200009; const ll INF=0x3f3f3f3f3f3f3f3f; int tot,head[N],nxt[N],to[N]; ll n,k,now,d[N],c[N],s[N],w[N],st[N],ed[N],f[N]; struct segment_tree{ int l,r; ll val,tag; }t[N*4]; void addedge(int x,int y){ nxt[++tot]=head[x]; head[x]=tot; to[tot]=y; } void init(){ n++,k++; d[n]=INF;w[n]=INF; for(int i=1;i<=n;i++){ st[i]=lower_bound(d+1,d+1+n,d[i]-s[i])-d; ed[i]=lower_bound(d+1,d+1+n,d[i]+s[i])-d; if(d[ed[i]]>d[i]+s[i]) ed[i]--; addedge(ed[i],i); } } void pushup(int p){ int ls=p*2,rs=p*2+1; t[p].val=min(t[ls].val,t[rs].val); } void pushdown(int p){ int ls=p*2,rs=p*2+1; if(t[p].tag){ t[ls].val+=t[p].tag;t[ls].tag+=t[p].tag; t[rs].val+=t[p].tag;t[rs].tag+=t[p].tag; t[p].tag=0; } } void build(int p,int l,int r){ t[p].l=l;t[p].r=r;t[p].tag=0; if(l==r){t[p].val=f[l];return ;} int mid=l+(r-l)/2; build(p*2,l,mid); build(p*2+1,mid+1,r); pushup(p); } void update(int p,int L,int R,int c){ if(L>R) return ; if(L<=t[p].l&&R>=t[p].r){t[p].val+=c;t[p].tag+=c;return ;} pushdown(p); int mid=t[p].l+(t[p].r-t[p].l)/2; if(L<=mid) update(p*2,L,R,c); if(R>mid) update(p*2+1,L,R,c); pushup(p); } ll query(int p,int L,int R){ if(L>R) return INF; if(L<=t[p].l&&R>=t[p].r) return t[p].val; pushdown(p); int mid=t[p].l+(t[p].r-t[p].l)/2; ll res=INF; if(L<=mid) res=min(res,query(p*2,L,R)); if(R>mid) res=min(res,query(p*2+1,L,R)); return res; } int main(){ ios::sync_with_stdio(false); cin>>n>>k; for(int i=2;i<=n;i++) cin>>d[i]; for(int i=1;i<=n;i++) cin>>c[i]; for(int i=1;i<=n;i++) cin>>s[i]; for(int i=1;i<=n;i++) cin>>w[i]; init(); for(int i=1;i<=n;i++){ f[i]=now+c[i]; for(int j=head[i];j;j=nxt[j]){ int y=to[j]; now=now+w[y]; } } ll ans=f[n]; for(int i=2;i<=k;i++){ build(1,1,n); for(int j=1;j<=n;j++){ f[j]=query(1,1,j-1)+c[j]; for(int k=head[j];k;k=nxt[k]){ int y=to[k]; update(1,1,st[y]-1,w[y]); } } ans=min(ans,f[n]); } cout<<ans; return 0; }
二. 单调队列优化
1. 单调队列
顾名思义,单调队列的重点分为「单调」和「队列」。
「单调」指的是元素的「规律」——递增(或递减)。
「队列」指的是元素只能从队头和队尾进行操作。
最大子序列之和
一个长度为
定义
枚举右端点
对于
我们对每个
- 将队头每一个距离超过
的数值弹出 - 此时队头就是答案。
- 不断删除队尾,直到队尾对应的
值小于 ,加入 。
例题 T331286
将工匠按照
第
第
如果第
可以把
比较任意两个决策
综上,可以维护一个
整个算法的时间复杂度为
#include<bits/stdc++.h> using namespace std; typedef long long ll; struct worker{ll s,l,p;}w[109]; ll n,m,f[109][16009],q[16009]; bool cmp(const worker&a,const worker&b){return a.s<b.s;} ll calc(ll i,ll k){return f[i-1][k]-w[i].p*k;} int main(){ ios::sync_with_stdio(false); cin>>n>>m; for(ll i=1;i<=m;i++) cin>>w[i].l>>w[i].p>>w[i].s; sort(w+1,w+1+m,cmp); for(ll i=1;i<=m;i++){ ll l=1,r=0; for(ll k=max(0ll,w[i].s-w[i].l);k<=w[i].s-1;k++){ while(l<=r&&calc(i,q[r])<=calc(i,k)) r--; q[++r]=k; } for(ll j=1;j<=n;j++){ f[i][j]=max(f[i-1][j],f[i][j-1]); if(j>=w[i].s){ while(l<=r&&q[l]<j-w[i].l) l++; if(l<=r) f[i][j]=max(f[i][j],calc(i,q[l])+w[i].p*j); } } } cout<<f[m][n]; return 0; }
例题 T331284
设
若采用枚举
基于“及时排除不可能的决策”,需要考虑一个决策
假设
设决策
首先,既然上式成立,则
如果上述两个命题同时成立,即
所以,如果要确保
(即 是满足 中最小的 )
如何维护这两个条件:
- 只需预处理出对于每个
,满足 中最小的 ,即为 ,在计算 时对 进行转移。 - 当一个新决策
进入候选集合时,若候选集合中有一个 满足 且 ,则 可悲排除。
综上所述,只需维护一个
但转移方程中的
最后,关于
- 使用 ST 算法预处理,
查询。 - 单调队列中某一项的
的结果就是单调队列中下一个元素的 值。
时间复杂度为
#include<bits/stdc++.h> using namespace std; typedef long long ll; const int N=1e5+9; const ll INF=0x3f3f3f3f3f3f3f3f; struct node{ ll id,x,y; bool operator<(const node&a)const{ return x+y>a.x+a.y; } }; ll n,m,lft=1,rght=0,q[N<<3],a[N],sum[N],lg[N],st[N][30],c[N],f[N]; bool vst[N]; priority_queue<node> qe; void init(){ lg[1]=0; for(int i=2;i<=n;i++) lg[i]=lg[i>>1]+1; for(int i=1;i<=n;i++) st[i][0]=a[i]; for(int i=1;i<=lg[n];i++){ for(int j=1;j<=(n-(1<<i)+1);j++){ st[j][i]=max(st[j][i-1],st[j+(1<<(i-1))][i-1]); } } } ll query(ll l,ll r){ ll len=lg[r-l+1]; return max(st[l][len],st[r-(1<<len)+1][len]); } int main(){ ios::sync_with_stdio(false); cin>>n>>m; for(int i=1;i<=n;i++){ cin>>a[i]; if(a[i]>m){cout<<-1;return 0;} sum[i]=sum[i-1]+a[i]; } init(); c[0]=1; for(int i=1;i<=n;i++){ for(int j=c[i-1];j<=n;j++){ if(sum[i]-sum[j-1]<=m){c[i]=j;break;} } } for(int i=1;i<=n;i++){ f[i]=min(INF,f[c[i]-1]+query(c[i],i)); while(lft<=rght&&sum[i]-sum[q[lft]]>m){vst[q[lft]]=1;lft++;} while(lft<=rght&&a[q[rght]]<a[i]){vst[q[rght]]=1;rght--;} q[++rght]=i; if(lft<rght) qe.push((node){q[rght-1],f[q[rght-1]],a[i]}); while(!qe.empty()&&(vst[qe.top().id]||qe.top().y<query(qe.top().id+1,i))) qe.pop(); if(!qe.empty()) f[i]=min(f[i],qe.top().x+qe.top().y); } cout<<f[n]; return 0; }
例题 P2627
令
把
可以用单调队列优化
#include<bits/stdc++.h> using namespace std; typedef long long ll; int l=0,r=1,n,k,e[100009],q[100009]; ll sum[100009],f[100009],g[100009]; int main(){ ios::sync_with_stdio(false); cin>>n>>k; for(int i=1;i<=n;i++) cin>>e[i]; for(int i=1;i<=n;i++) sum[i]=sum[i-1]+e[i]; for(int i=1;i<=n;i++){ g[i]=f[i-1]-sum[i]; while(l<=r&&q[l]<i-k) l++; while(l<=r&&g[q[r]]<g[i]) r--; q[++r]=i; f[i]=g[q[l]]+sum[i]; } cout<<f[n]; return 0; }
总结
当状态转移方程形如
- 决策
的取值范围 和 是关于变量 的一次函数且具有单调性,即窗口长度保持不变。 - 优化的关键部分
是关于变量 和 的多项式函数,
就可以使用单调队列进行优化。
一般而言,
- 对于第一部分仅与
相关,无论采取哪个 ,第一部分均相等,这样可以选出最优决策后再累加。 - 对于第二部分仅与
相关,当 发生改变时不会发生变化,这样保证原来较优的决策能保持最有,这样可以保持单调队列的单调性,及时排除不可能的决策。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探