动态规划——从菜鸟到入门
什么是动态规划?
动态规划,英文名为Dynamic Programming,又称DP(当然小写的dp也行),是一种通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。由于动态规划并不是某种具体的算法,而是一种解决特定问题的方法,因此它会出现在各式各样的数据结构中,与之相关的题目种类也更为繁杂。在 OI 中,计数等非最优化问题的递推解法也常被不规范地称作 DP。
dp的重要性?
在作者打过的镇赛,区赛乃至GDOI,CSP中,dp一直是常考的东西。所以学会dp尤为关键。
举个例子,2017-2022年的镇赛,区赛中dp常作为压轴题出现,甚至NHOI 2022中T5,T6都是dp。
动态规划入门
先来看这样一道题:P1216 [USACO1.5][IOI1994]数字三角形 Number Triangles(学校OJ 1351)
尽管这道题是IOI的,但不妨碍我们切掉
想必有人会考虑贪心,即如果左边大一点就往左边走,右边大一点就忘右边走。看到给出的样例,就知道这个方法不可行了。如果还想继续从第二大值,第三大值等等情况考虑,你就发现深陷其中无法自拔了。
接下来我们通过这道例题详细分析以下如何做一道动态规划的题目:
Step 1:设计状态
对于动态规划,设计一个状态是做题的第一步。这里初学者可能不知道状态是什么,但是请先耐心看下去。
比如这道题,我们定义一个二维数组
一般而言,设计的状态通常都是由所需要的答案决定,如本题需要的就是从起点
请注意,设计出一个好的状态非常重要!请注意,设计出一个好的状态非常重要!请注意,设计出一个好的状态非常重要!(重要的话说三遍!!!)
Step 2:设计转移方程
显然,设计好一个状态后,我们需要考虑这个状态的答案应该如何计算得到的,即设计转移方程。
例如本题,我们可以画一个表格分析。我们可以自行在电脑前画出这个表格。
显然,我们发现,有两种可以走到当前点
,这时问题转化为从 走到 的最大值,最后再加上当前取 格子的价值 ,即 。 ,这时问题转化为从 走到 的最大值,最后再加上当前取 格子的价值 ,即 。
我们要的是最大值,所以对两种情况取一个最大值即可得到当前状态
或者可以写成
f[i][j]=max(f[i-1][j-1]+a[i][j],f[i-1][j]+a[i][j]);//写法1
f[i][j]=max(f[i-1][j-1],f[i-1][j])+a[i][j];//写法2
Step 3:确定初始化
这个转移方程是一定要有初始化的。显然我们的起点为
当然,我们也可以认为,main
函数外面,就不需要了,因为初始的时候就已经等于
Step 4:确定答案
根据题意,我们知道终点可以是最后一行的任意一个格子,即
#include<bits/stdc++.h>
using namespace std;
const int N=1005;
int n;
int a[N][N],f[N][N];
int ans;
int main(){
// f[0][0]=f[0][1]=0;//初始化
scanf("%d",&n);
for(int i=1;i<=n;i++)
for(int j=1;j<=i;j++)
scanf("%d",&a[i][j]);
for(int i=1;i<=n;i++)
for(int j=1;j<=i;j++)
f[i][j]=max(f[i-1][j-1],f[i-1][j])+a[i][j];
for(int i=1;i<=n;i++)ans=max(ans,f[n][i]);
printf("%d",ans);
return 0;
}
总结
这是我总结的dp的四步走,熟记这四个步骤你可以完成大部分的简单dp,从而入门dp。
对于这道题的一个题外话
如果设计了不一样的状态,那么我们就可能会有不一样的转移方程。其实即使设计了一样的状态,也可能会有不一样的方程,而最后的答案也可能会有所不同。
如本题可以用倒推的方案去设计,即从这两个方案入手。最后的答案时结尾具体代码就不展示了,留给读者自行思考吧。
练习
下面给几道例题巩固一下:
-
Frog 1(学校OJ 1352)
-
Frog 2(学校OJ 1353)
-
P1115 最大子段和(学校OJ 1239)
-
P2800 又上锁妖塔(学校OJ1355)
Frog 1
按照刚才的步骤,我们先设计一下状态。设
显然,第
初始化为
而最后的答案就为
Frog 2
和上面那一题差不多,区别就是能跳
最大子段和
虽然这道题是可以不用动态规划来解的,但是为了锻炼我们的dp能力,所以还是试一下吧。这题还是比较经典的,并且请注意一下我下面的用词。
设
对于第
- 接上以第
个数为末尾的情况,问题转化为以第 个数为结尾的最大的答案,答案应该为 - 直接单独成立一个,答案即自己本身
。
也是取两种方案的最大值就可以了。
初始化为
需要注意的是,这里的答案并不是
for(int i=1;i<=n;i++)
{
scanf("%d",&a);
f[i]=max(f[i-1],0)+a;//可以发现,甚至不需要开数组
ans=max(ans,f[i]);
}
printf("%d",ans);
又上锁妖塔
这道题和我们前面做的有一点不同,因为他有一个小小的限制,即每跳跃一次后就不能再进行跳跃了。如果我们还像上面那样设计状态的话,显然是不能处理这个限制的。
但是,我们可以开两个数组,从而做到分开设计啊。。。
设
设
这样我们就可以分开讨论啦。
根据题意,每次跳跃可以跳
而爬就没有限制了,爬的上一个操作既可以是跳跃,也可以是爬,所以类似的,问题转化为跳到
同样的道理,第
至于初始化。。。自己想吧(doge
背包问题
背包问题是动态规划的一个小分支。其类型都可以转化成这样:
给一个体积为
根据物品的数量,背包问题又分成以下三种。
- 0/1背包:每种物品的数量只有一个。
- 多重背包:会给出每种物品的数量。
- 完全背包:每种物品的数量有无限个。
0/1背包
首先给出一道经典题目(学校OJ1354),这道题目足够经典了吧。。。
解释一下下面的
首先还是我们的第一步,设计状态。设计
接下来,第二步考虑如何转移。对于当前的第
- 选,前提是
,那么就将问题转化为将前 个物品放入体积为 的背包能获得的最大价值,此时 - 不选,那么就将问题转化为将前
个物品放入体积为 的背包能获得的最大价值,此时
还是要取最大值,所以
for(int i=1;i<=n;i++)
{
for(int j=0;j<=V;j++)
{
f[i][j]=f[i-1][j];
if(j>=w)f[i][j]=max(f[i][j],f[i-1][j-v]+w);
}
}
不难理解,答案就是
时间/空间复杂度为
优化空间
事实上,我们可以只用一维数组来表示这些状态,即可以将第一维的
我们可以看到,要想得到
当我们使用一维数组存储状态时,
不过需要注意的是,当我们把二维数组变成一维数组后,需要仔细思考一下
优化了空间后,变成了
for(int i=1;i<=n;i++)
{
for(int j=V;j>=0;j--)
if(j>=v)f[j]=max(f[j],f[j-v]+w);
/*
或者可以这么写:
for(int j=V;j>=v;j--)f[j]=max(f[j],f[j-v]+w);
*/
}
这里详细解释一下,为什么是倒序。
尝试自己模拟算出
比如现在举一个例子,
不过正序也是有用的,适合于完全背包。
多重背包
具体的什么不多说,其实就是0/1背包再加一个数量而已,即再加一个循环。
for(int i=1;i<=n;i++)
{
for(int k=1;k<=num;k++)//物品的数量
for(int j=V;j>=v;j--)
f[j]=max(f[j],f[j-v]+w);
}
时间复杂度为
这对于一般的多重背包也是够的了,但是还是有优化的方式,使得这个时间复杂度能更小一点,比如可以使用二进制优化的方法去使其优化到
那既然写了,就顺嘴提一下吧。首先将数量
不难发现,用这里任意的数组合可以得到
这只是题外话了,具体我们只要掌握一开始给的那种就行了。
完全背包
经典题目(学校OJ1357)
这里不同的一点就是,数量是无限个的。但是显然不能装无限个物品,你会想到保留足够的数量就行了吧。比如这个背包的体积为
重点在这里。如果保留的数量依然很多个的话,那么这种方法就不太好用了。接下来给出完全背包的一种小优化。
其实前面也有提到,再
还是举
for(int i=1;i<=n;i++)
{
for(int j=v;j<=V;j++)f[j]=max(f[j],f[j-v]+w);
}
于是我们愉快的发现,时间复杂度又变成了
关于背包的初始化
emmm,上面忘记说了。
其实也挺简单的
memset(f,0,sizeof(f));
因为根据定义,
练习
P1049 [NOIP2001 普及组] 装箱问题(0/1背包,不过要清楚这时候状态该是什么?初始化应该是什么?)(学校OJ1356)
P1679 神奇的四次方数(告诉你们,mwj不会这道题)(学校OJ1358)
课后例题
既然都学会了,那么来试一试真题吧。
P8816 [CSP-J 2022] 上升点列(学校OJ1294)
结言
dp的路上还很漫长,但是绝对也值得期待。
本来还想继续写各种dp的,比如区间dp,树形dp,状压dp,数位dp,等等。但是因为就这样吧。实质是我鸽了,有时间就写吧。
话说不放学校OJ的网站真不是我懒,而是我怕有别的人过来乱搞。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)