导读
信息学能够有助于孩子未来工作发展,提升孩子的综合能力。
上一节课,我们讲了动态规划的基本理论,并做了简单的练习,这节课,我们深入讲解动态规划题型,讲解题目特点,并通过实例讲解做题方法。
1 动态规划回顾
首先我们先来复习一下动态规划吧。
动态规划(Dynamic Programming,DP)是运筹学的一个分支,是求解决策过程最优化的过程。
动态规划有如下重要概念:
1、阶段
2、阶段变量
3、状态
4、无后效性
5、决策
6、策略
7、最优策略
8、最优化原理
9、状态转移方程
动态规划局限性如下:
没有统一的处理方法,难以应用
存在维数障碍
2 动态规划答题方法
我们先看一下答题方法,后面通过实际的题目实战一下!
1 动态规划题目类型
动态规划题型千奇百怪,但是总体来说,总体有如下几种:
线性动态规划
区间动态规划
树形动态规划
背包动态规划
线性动态规划是指目标函数为特定变量的线性函数,约束是这些变量的线性不等式或等式,目的是求目标函数的最大值或最小值。
区间动态规划的状态表示一般为d[i][j],表示区间[i, j]上的最优解,然后通过状态转移计算出[i+1, j]或者[i, j+1]上的最优解,逐步扩大区间的范围,最终求得[1, len]的最优解。区间动态规划也被拓展为表示索引为i的项和索引为j的项之间的最优解。例如将i分为j份。从将1分为1份一直推导到将n分为m份。
树形动态规划是指在树结构上做动态规划。树本身就具有子结构,非常容易和动态规划结合。但是树形动态规划一般也要和递归算法那结合。所以树形动态规划一般较难。
背包动态规划是非常经典的动态规划。背包动态规划一般也叫背包问题,这类问题的特点是有一个上界,并且每一种情况不可分。如果使用贪心难以达到全局最优的情况。需要使用动态规划去考虑。
除此之外,我们还有状态压缩动态规划、动态规划优化问题等。
2 动态规划题目特点
题目要满足如下几个特点才可以使用动态规划:
重叠子问题
最优子结构
无后效性
我们上节课有讲过最优子结构和无后效性的概念,这里我们说一下重叠子问题。
前面我们的例子中,例如我们找15分为1,5和11,最好情况运算的过程中,我们在计算后面的时候,都会用到前面的情况。例如f[10]是f[12]的子问题,也是f[13]的子问题。
3 动态规划步骤
动态规划题目,我们虽然没有固定的解题方法,但是通用的步骤还是有的。
一般来说,动态规划题目有如下几个步骤:
1、明确子结构是什么
2、明确子结构怎么表示
3、明确子结构间的关系
4、明确子结构范围
在我们前面的引入的例子中,每个宝藏的重量就是子结构。然后我们使用数组来表示子结构。然后我们定义了状态转移方程,就是子结构之间的关系。我们上界是15,这个就是我们子结构的范围。
其中最重要的是第三步,也就是确定状态转移方程,具体原因我们会在下面的题目中讲解。
3 动态规划经典例题
本节课的作业,就是复习上面的所有知识,并完成下面两道题目!
1 分苹果
小明现在有一框苹果,一共有n个,现在要把这框苹果分为m堆,每堆至少有一个苹果。小明想知道一共有多少种分法,所以过来找你帮忙。要求任意两种分法不能相同。
例如:小明有10个苹果,要分3堆,下面这几种分法算作一种:
2,2,6;
2,6,2;
6,2,2;
【分析】
这道题目是非常经典的区间动态规划。
我们可以使用枚举,遍历所有情况,并且要求后面的数据要大于等于前面的数据,这样枚举也是可以的。
例如,将10个分成3份:
1 1 8
1 2 7
1 3 6
1 4 5
2 2 6
2 3 5
2 4 4
3 3 4
但是因为具体的份数没有,所以使用循环实现枚举不现实,可以使用递归。
但是这道题目更合适的方式是使用动态规划。
其实一道题目中,找到子结构以及子结构的表示方法不是最重要的,最重要的是我们要找到能够达成具有递推关系的状态转移方程。只要能找到这个方程,方程中满足递推关系并构成状态转移方程的每一个部分就是子结构。一般我们都用数组来描述。
这样,我们就解决了前三个部分,最后一个部分就是边界,也是终止条件。在我们这道题目中,我们要将n个苹果分成m堆,那么n和m就是边界。
所以我们把重心放在状态转移方程上面,状态转移方程也是动态规划的难点。
我们看一下将10个苹果分3堆,我们可以分两种情况:
至少有一堆有一个苹果
每一堆都至少有两个苹果
对于第一种情况,我们可以默认第一堆为一个苹果,剩下的两堆任意。那这样的情况,和我们把9个苹果分为后两堆是一样的。
对于第二种情况,我们可以让每一堆都减少一个苹果。这样依然能够保证每一堆有苹果。这样的情况,和我们把7个苹果分为3堆是一样的。我们给每一堆再各加一个苹果就是将10个苹果分成3堆并且保证每一堆至少有两个苹果。
所以我们有如下公式:
将10个苹果分3堆的情况 = 将9个苹果分2堆的情况 + 将7个苹果分3堆的情况
这就是我们的状态转移方程,子结构为将n个苹果分为m堆的情况数。我们用二维数组来表示子结构:
dp[n][m]:表示将n个苹果分为m堆
所以上述状态转移方程可以写为:
dp[i][j] = dp[i-1][j-1] + dp[i-j][j]
边界如下:
1<=i<=n;
1<=j<=min(i,m)
然后我们就可以遍历所有情况了。
我们要注意,对于dp[i][1]来说:
dp[i][1] = dp[i-1][0] + dp[i-1][1]
dp[i-1][0]要为0,然后dp[i-1][1]会一直计算到dp[1][1]:
dp[1][1] = 1
在我们的代码中,我们使用如下方式:
if (i==1&j==1) dp[i][j]=1;
一方面,这样我们循环就可以按照我们的边界开始,只需要给dp[1][1]单独赋值。另一方面,位运算的运算效率要高于逻辑运算。所以使用位运算做判断。
这道题目,有了上面的就非常简单了,我们直接给出代码:
#include<iostream>
using namespace std;
int n,m,dp[20005][20005];
int main(){
cin>>n>>m;
for (int i=1;i<=n;++i)
for (int j=1;j<=i&&j<=m;++j){
dp[i][j]=dp[i-1][j-1]+dp[i-j][j];
if (i==1&j==1) dp[i][j]=1;
}
cout<<dp[n][m]<<endl;
return 0;
}
我们也可以给f[i][1]单独赋值。
f[i][1] = 1
然后边界从2开始即可。
执行结果如下:
2 0/1背包
一个旅行者有一个最多能用m公斤的背包,现在有n件物品,它们的重量分别是W1,W2,...,Wn,它们的价值分别为C1,C2,...,Cn.若每种物品只有一件求旅行者能获得最大总价值。
【输入格式】
第一行:两个整数,M(背包容量,M<=200)和N(物品数量,N<=30);
第2..N+1行:每行二个整数Wi,Ci,表示每个物品的重量和价值。
【输出格式】
仅一行,一个数,表示最大总价值。
【样例输入】
10 4
2 1
3 3
4 5
7 9
【样例输出】
12
【分析】
因为每一个物品都只有一个,所以我们应该从物品的角度去遍历。考虑他们之间的组合是否是满足在背包重量之内,并且价值最大。如果比原有价值大,那么我们就可以替换原有的价值。
当我们要把第i个物品放入,如果能放入,那么:
当前最大价值 = max(新放入后的最大价值,之前的最大价值)
假如我们用f[j]来表示背包容量为j时的最大价值。w[i]表示物品i的重量,c[i]表示物品i的价值,那么:
f[j] = max(f[j - w[i]] + c[j], f[j])
这是时候,我们就要考虑边界,因为物品只有n个,所以i的边界就是n。背包的容量为m,所以背包的边界为m。
我们从放入不同的物品角度考虑。我们考虑每放进去一个物品,容量就会减少物品的重量,但是价值就会加上对应物品的价值。所以我们需要每次遍历物品的时候,都要去更新放入不同物品的价值。
代码如下:
#include<iostream>
using namespace std;
const int maxm = 2001, maxn = 31;
int m, n;
int w[maxn], c[maxn];
int f[maxm];
int main(){
cin>>m>>n;
for (int i=1; i <= n; i++)
cin>>w[i]>>c[i];
for (int i=1; i <= n; i++)
for (int j = m; j >= w[i]; j--)
if (f[j-w[i]]+c[i]>f[j])
f[j] = f[j-w[i]]+c[i];
cout<<f[m];
return 0;
}
执行结果如下:
4 作业
本节课的作业,就是复习上面的所有知识,并完成下面的题目!
1 完全背包
设有n种物品,每种物品有一个重量及一个价值。但每种物品的数量是无限的,同时有一个背包,最大载重量为M,今从n种物品中选取若干件(同一种物品可以多次选取),使其重量的和小于等于M,而价值的和为最大;
【输入格式】
第一行:两个整数,M(背包容量,M<=200)和N(物品数量,N<=30);
第2..N+1行:每行二个整数Wi,Ci,表示每个物品的重量和价值。
【输出格式】
仅一行,一个数,表示最大总价值。
【样例输入】
12 4
2 1
3 3
4 5
7 9
【样例输出】
15
AI与区块链技术