DP学习笔记
动态规划算法与分治法类似,是将待求解的问题分解为若干个子问题(阶段),按顺序求解子阶段,前一子问题的解,为后一子问题的求解提供了有用的信息。在求解任一子问题时,列出各种可能的局部解,通过决策保留那些有可能达到最优的局部解,丢弃其他局部解。依次解决各子问题,最后一个子问题就是初始问题的解。
动态规划做题思路:
-
考虑
数组每一维状态表示什么 -
初始化,如果要取
, 数组要赋较大的值。 的值也要考虑 -
想转移顺序,逆序or正序。具体想
的范围 -
想状态转移方程(重点)
-
想最终结果,一般都表示为
之类的。。。
PS.由于作者做题经验不足且很懒,有些地方直接就放其他大佬(orz)的博客了,请见谅。
1.线性dp
定义:线性这些都是没用的不要看
线性
最终结果为
其实也就是for for if。。。
例1:最长上升子序列(划重点)
#include<bits/stdc++.h> using namespace std; int n, a[50005], f[50005], ans, x, t; int main() { cin>>n; while(cin>>x) { t++; a[t]=x; if(cin.get()=='\n') { break; } } f[1]=1; for(int i=2;i<=n;i++) { f[i]=1; for(int j=1;j<i;j++) { if(a[i]>a[j]&&f[i]<f[j]+1) { f[i]=f[j]+1; } } } for(int i=1;i<=n;i++) { ans=max(ans, f[i]); } cout<<ans; return 0; }
比较好理解。
例2:合唱队形
思路:先来一遍最长上升子序列,再来一遍最长不上升子序列,最后枚举求值(题解)
#include<bits/stdc++.h> using namespace std; int ans, n, a[500005], dp1[500005], dp2[500005]; int main() { cin>>n; for(int i=1;i<=n;i++) { cin>>a[i]; } for(int i=1;i<=n;i++) { dp1[i]=1; dp2[i]=1; } for(int i=n-1;i>=1;i--) { for(int j=i+1;j<=n;j++) { if(a[i]>a[j]) { dp1[i]=max(dp1[i],dp1[j]+1); } } } for(int i=2;i<=n;i++) { dp2[i]=1; for(int j=1;j<=i;j++) { if(a[i]>a[j]) { dp2[i]=max(dp2[i],dp2[j]+1); } } } for(int i=1;i<=n;i++) { ans=max(ans,dp1[i]+dp2[i]-1); } cout<<n-ans; return 0; }
2.区间dp
很惊讶以前甚至都没有写区间dp的总结。
简而言之,这种dp一般开二维,枚举区间,枚举断点,合并求值。(如
模版代码:
for(int len=1;len<=n;len++) { for(int i=1;i<=n-len+1;i++) { int j=i+len-1; for(int k=i;k<j;k++) { //状态转移方程 } } }
例题:
一些套路:
- 基本都有枚举断点、合并的操作。状态转移方程:
。 - 除了上一条之外,还可以从两边的端点扩展,或者同时扩展。具体根据题目改变。状态转移方程:
。 - 转移的时候往往要分类讨论,或者判断是否合法。注意,像
这种语句 不能拿出来另外判断! - 如果实在难以维护,可以考虑多维护一维/多开一个数组,辅助转移。
- 有时候预处理能省很多事。
- 还有一个关路灯型的区间dp,很多题里都有。
总结:区间dp的数据范围一般比较小(甚至有些题
3.背包dp
直接看吧。。。
如果你还是不理解多重背包的二进制优化,可以看看这篇。
4.树形dp
个人认为归纳梳理很清晰
树形dp
大致就是dfs上写dp(可以用链式前向星来存图)
5.状态压缩dp
这是一个考试可以打暴力分的优化。本质上是dp,通过状态压缩优化,将一些操作改为位运算即可。(注意优先级)
先放个模板题和代码:
#include<bits/stdc++.h> #define ll long long using namespace std; ll n, k, ans, len, num; ll c[10000], dp[10][100][2000], cnt[10000]; int main() { cin>>n>>k; for(int i=0;i<(1<<n);i++) { int s1=i; num=0; while(s1) { if(s1&1) { num++; } s1>>=1; } cnt[i]=num; if((((i<<1)|(i>>1))&i)==0) { c[++len]=i; } } dp[0][0][0]=1; for(int i=1;i<=n;i++) { for(int d=1;d<=len;d++) { int s1=c[d]; for(int u=1;u<=len;u++) { int s2=c[u]; if(((s2|(s2<<1)|(s2>>1))&s1)==0) { for(int j=0;j<=k;j++) { if(j-cnt[s1]>=0) { dp[i][j][s1]+=dp[i-1][j-cnt[s1]][s2]; } } } } } } for(int i=1;i<=len;i++) { ans+=dp[n][k][c[i]]; } cout<<ans; return 0; }
6.单调队列优化dp
前置知识:单调队列(详见学习笔记)
其实它本质上是一个dp,由于时间复杂度的限制,用单调队列优化。
往往我们要求最大值,就把最大值放在队首,维护一个单调递减的队列。反之,亦然。
核心代码:
for(int i=1;i<=n;i++) { if(!q.empty()&&q.front()+m<i)//m:区间长度 { q.pop_front(); } //状态转移方程 while(!q.empty()&&a[q.back()]>/<a[i]) { q.pop_back(); } q.push_back(i); }
7.斜率优化dp
你猜我为什么没有写 完
似乎这种优化只需要掌握一点关于一次函数的知识,实在不知道的bd一下就好了。
进入正题:
它的状态转移方程一般为:
此时这个式子需要采用斜率优化。
做题思路:先用常规dp方法推出状态转移方程,假设存在
然后判断要维护一个上凸包还是下凸包(求最大值为上凸包,求最小值为下凸包),用单调队列来维护点集。
就长这个样子:
模板代码:
int slope_up(int j, int k) { return dp[j]-dp[k]; } int slope_down(int j, int k) { return a[j]-a[k]; } int main() { memset(dp, INF, sizeof(dp)); dp[0]=0; int head=1, tail=0; q[++tail]=0; for (int i=1;i<=n;i++) { while(head<tail&&slope_up(q[head+1],q[head])<=b[i]*slope_down(q[head+1],q[head])) { head++; } //转移方程 while(head<tail&&slope_up(q[tail],q[tail-1])*slope_down(i,q[tail])>=slope_up(i,q[tail])*slope_down(q[tail],q[tail-1])) { tail--; } q[++tail]=i; } }
方面的优化汇总
tbc.
是绳子都会断,是糖都会过期。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!