动态规划 之《从入门到入土》
bushi
动态规划的几个模板 and 例题
背包问题
01背包
顾名思义,一个东西只有选和不选两种选择。
求体积一定的包里能放的最大质量。
for(int i=1;i<=n;i++) { for(int j=m;j>=w[i];j--)//w[i]表示物品 i 的体积 { f[j]=max(f[j],f[j-w[i]]+v[i]);//v[i]表示物品 i 的质量 //f[j]表示 背包容量为 j 时最多能放的质量 } }
为什么要逆序
首先,通过上一个问题,我们确认了我们目前一维的dp数组,保存的是确认过的最新一层的数据,即上一层的数据。
当我们计算当前层时,对于二维时的状态转移方程有
;
可以看到,使用的上一层的原始数据 ,而我们使用一维的状态转移方程时有
;
当我们从小到大更新是, 因为是严格小于 的,所以我们可以举个例子:
, 因为我们是从小到大更新的,所以当更新到 的时候, 已经更新过了,已经不是上一层的 。
而当我们逆序更新时有,举例。当更新 时, 还没有被更新,还是上一层的数据,这样才能保证没有读入脏数据。
以上来自 AcWing
完全背包
与 01 背包的区别就是:同一个物品可以被放多次
for(int i=1;i<=n;i++) { for(int j=w[i];j<=m;j++)//这里的枚举顺序要改变一下 { f[j]=max(f[j],f[j-w[i]]+v[i]); //f[j]表示总体积是 j 时最大价值是多少 } }
枚举顺序改变的原因:
每次再使用
的时候状态就是已经更新过的
多重背包
在完全背包的基础上,每个物品是有限定数量的。
(1) 可以把每种物品看成 cnt 个物品,只不过他们的质量体积都相同,这样就能转化为 01 背包问题。
(2) 每个物品有 不选、选一个、选两个、………… 选 cnt 个,加一重循环就能实现。
好像两个运行时间差不多?
代码(方法 2 )
for(int i=1;i<=n;i++) for(int k=1;k<=cnt[i];k++) //物品数量,可以看作一个物品有好多份,01背包 { for(int j=m;j>=k*w[i];j--)//容量 { f[j]=max(f[j],f[j-w[i]]+v[i]); } }
分组背包
思路:每一组物品有
不选、选第一个、选第二个、…… 选第 cnt 个。(01背包)
- 代码
for(int i=1;i<=n;i++)//枚举每一组 { int cnt; cin>>cnt; for(int j=1;j<=cnt;j++) { cin>>w[j]>>v[j];//输入 } for(int j=m;j>=0;j--)//枚举背包容积 { for(int k=1;k<=cnt;k++)//枚举每一种决策 { if(j>=w[k]) f[j]=max(f[j],f[j-w[k]]+v[k]); } } } cout<<f[m]<<endl;
例题:
这里 https://www.luogu.com.cn/training/572428
子序列问题:
最长上升子序列长度
- 朴素做法
表示 以 点为结尾的最长上升子序列的长度。
for(int i=1;i<=n;i++) { dp[i]=1; for(int j=1;j<i;j++) { if(a[j]<a[i])dp[i]=max(dp[i],dp[j]+1); } maxn=max(maxn,dp[i]); }
- 优化:
如果 a_i 大于原序列末尾,就可以把他直接加上;
如果小于原序列末尾,二分查找到此时序列中第一个小于等于它的值,替换掉。
理由:
如果y在末尾,由于y < a[i],所以y后面能接的不如a[i]多,y让位给a[i]可以让序列更长
如果y不在末尾,那y有生之年都不会再被用到了,直接踹了y就行,y咋样,who care?
p[0]=-1; for(int i=1;i<=n;i++) //求 最长上升子序列的长度 { if(a[i]>p[cnt])p[++cnt]=a[i];//直接加在后面 else{ // 查找第一个 >= a[i] 的元素的位置 int left=1,right=cnt; while(left<right) { int mid=left+right>>1; if(p[mid]>=a[i]) right=mid; else left=mid+1; } p[left]=a[i]; //替换掉 } }
其他什么上升下降都差不多,就不再说了
最少的不上升子序列的个数
Dilworth 定理
对偏序集
,设 中最长链的长度是 ,则将 中元素分成不相交的反链,反链个数至少是 。
人话翻译:最少的不上升子序列的个数就是最长上升子序列的长度
具体证明我不会请参考 https://www.luogu.com.cn/article/znt20wu4
for(int i=1;i<=n;i++)//求最长不下降子序列 { if(a[i]<=d[cnt]) d[++cnt]=a[i]; else{ int left=1,right=cnt; while(left<right)//查找第一个 < a[i] 的元素的位置 (因为等于的时候不需要替换) { int mid=left+right>>1; if(d[mid]<a[i])right=mid; else left=mid+1; } d[left]=a[i]; } }
区间DP
性质
- 合并:将两个或者多个部分进行整合,当然也可以反过来;
- 特征:能将问题分解成两两合并的形式;
- 求解:对整个问题设最优解,枚举合并点,将问题分解成左右两个部分,最后合并两个部分的最优值得到原问题的最优值。
定义:
定义
以上来自 OIWIKI
例题:
- 观察到是环,首先
ctrl + c,ctrl + v复制一份接在后面 - 最普通的算法O(n^3):224ms
其中, 代表 到 堆的最优值, 代表第 堆到第 堆的数目总和。有: 。 - 代码:17ms
#include<bits/stdc++.h> using namespace std; int n; int a[250]; int sum[250]; int dp[250][250]; int dp2[250][250]; int minn=INT_MAX; int maxn=INT_MIN; signed main() { memset(dp2,0x3f3f3f,sizeof(dp2)); cin>>n; for(int i=1;i<=n;i++) { cin>>a[i]; a[n+i]=a[i];//复制一份接在后面 } int N=2*n; for(int i=1;i<=N;i++) { sum[i]=sum[i-1]+a[i]; dp2[i][i]=0; } for(int k=2;k<=n;k++) //区间长度 { for(int i=1;i<=N-k+1;i++) //枚举左端点 { int j=i+k-1; for(int p=i;p<j;p++) //枚举中间的序号 { dp[i][j]=max(dp[i][j],dp[i][p]+dp[p+1][j]+sum[j]-sum[i-1]); //最大值 dp2[i][j]=min(dp2[i][j],dp2[i][p]+dp2[p+1][j]+sum[j]-sum[i-1]); //最小值 } } } for(int i=1;i<=n;i++)//开始点 { int p=i+n-1;//结束点 maxn=max(maxn,dp[i][p]); minn=min(minn,dp2[i][p]); } cout<<minn<<endl; cout<<maxn<<endl; return 0; }
练习题:
直接去洛谷搜标签
树形 DP
有依赖的背包问题
意思:给你一棵树,选取一个节点就必须选取他的父亲节点,求物品总体积不超过背包容量,且最大的总价值。
思路:
- 定义
表示以 为根的子树,总共体积不超过 的最大价值 - 初始化要注意,
时并不是所有的 都为0,
因为不选就整颗子树就都不能选了,
所以计算前只有 , ;
其他的都置为无穷小就行了 - 然后我们还需要一个
来表示: 当前这个根节点的到第 个儿子时,用 的空间所获得的最大价值
然后在遍历子节点的时候更新一下 数组
具体就是 ( 是第 个儿子的编号)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!