目录
前言
毕业了开始找工作了。才发现自己对于算法理论这块还是很薄弱,一边学一边找吧。
前几天刷题的时候发现动态规划比较难懂,虽然有不少题解但讲解的都不是很详细,所以自己开个帖子记录一下在学习的过程中遇到的难点和自己的理解。
关于动态规划的基础概念在代码随想录中写的很详细,建议初次接触动态规划的可以去看看。
动态规划的解题步骤
- 确定dp数组(dp table)以及下标的含义
- 确定递推公式
- dp数组如何初始化
- 确定遍历顺序
- 举例推导dp数组
01背包理论
简单的例子
题目是:背包最大重量为4。物品为:
重量 | 价值 | |
---|---|---|
物品0 | 1 | 15 |
物品1 | 3 | 20 |
物品2 | 4 | 30 |
问背包能背的物品最大价值是多少?
定义递推数组及其下标的含义
对于背包问题,有一种写法, 是使用二维数组。创建一个dp[i][j]来存放我们想要的结果,i表示在0到i个物品里面任意取物品,j表示当前的书包的重量。
即dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。
把dp数组给画出来 以图作为例子:
dp[i][j] | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
物品0 | a | b | c | ||
物品1 | d | e | f | ||
物品2 |
图中的a表示在物品选择范围在物品0到物品0的时候,背包重量j为0的条件下,能取到的最大价值总和。
说人话就是:你只能选物品0(物品i=0)并且背包里不能装东西(背包重量j=0),这时候的你的包里,也就是dp[0][0]为多少啊?那当然是0啊。
同理,dp[0][]的意思就是,只能选物品0的时候,背包重量为0,1,2,3,4的时候,最大的价值总和是多少。
即物品选取范围–i不发生改变,j背包重量发生了变化,最大的价值总和是多少。
根据物品选取范围 i,书包重量 j 和价值总量 dp[i][j] 的定义,结合题目给出的条件,能得到dp[0][]的值。因为背包问题中每个物体只能拿取一次的限制条件,从背包重量为1以后,最大价值总和就不变了,因为已经没有可以放东西到背包里面了。
dp[i][j] | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
物品0 | 0 | 15 | 15 | 15 | 15 |
接下来是背包重量 j=0,物品选取范围 i 发生改变。那很简单,因为背包放不进去东西,怎么选取东西都放不进。所以价值总和为0。dp数组的结果如下图:
dp[i][j] | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
物品0 | 0 | 15 | 15 | 15 | 15 |
物品1 | 0 | a | b | c | d |
物品2 | 0 | e | f | g | h |
颠倒顺序的初始化
这也就顺便完成了数组的初始化。给dp数组初始化的意义在于,动态规划的递推公式一般都是根据之前的状态的结果推导出当前状态的结果。即根据 dp[i-1][j] 或者 dp[i][j-1] 推导出dp[i][j]。比如:上图中想要得到a的结果,根据a的左上方已经得到的结果推导出a的结果。
确定递推公式
正常来说应该是先确定递推公式再根据递推公式确定哪些地方需要初始化。但是这里为了说明dp数组的各个参数的含义顺便就先做了初始化。
接下来我来尝试说明一下对于递推公式的推导。
首先确定你有几种选择
-
不放进当前物品i:
因为选择了不放进当前的i物品,所以背包里面的最大价值总和与dp上方数值相同,即dp[i][j]=dp[i-1][j];
-
放进当前物品i:
因为放进来新的物品,所以得重新计算背包中现有的最大价值总和。在这个问题中,有三个点需要注意。
1.需要明确i的变化。因为可以新增加一个物品,所以i肯定是+1了。
2.需要注意背包的重量。因为可以新增加一个物品,背包的重量要足够容得下新的物品。
3.需要注意最大价值总量的增加。新物品的加入肯定会引起背包里的最大价值总量的变化。放进当前物品 i 的递推公式:dp[i][j]=dp[i - 1][j - weight[i]] + value[i];
这个递推公式简单地来说就是 在决定讲新物品i放进背包时,新的背包的总价值dp[i][j]=之前背包总价值dp[i - 1][j - weight[i]] +新物品的总价值value[i]
所以完整的递推公式就是在这两个选项中选取最大价值总和高的那一项作为当前的最大价值总和:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
确定遍历顺序
在理解了dp[i][j]数组以及下标i,j的含义后,遍历顺序的选择也变的简单了起来,这道题的遍历是都可以的。
先遍历物品就是在背包的重量不变的情况下,选出最高价值的物品,然后再逐步增加背包重量,重复上一个过程。
先遍历背包就是在可以选择的物品范围不变的情况下,不同的背包容量中,怎么选取物品的价值量最高。再逐渐扩大选取物品的范围,重复比较过程。
举例推导dp数组
建议自己在纸上推导一遍,确定了递推公式后,手写举例一部分的数组。然后再开始编写程序。
如果最后结果做出来的和预计不符,可以选择打印程序中的数组,与自己推到的数组作比较。这样更容易发现是哪里出了问题。
做动态规划的题目,最好的过程就是自己在纸上举一个例子把对应的dp数组的数值推导一下,然后在动手写代码。
完整的代码
public static void main(String[] args) {
int[] weight = {1, 3, 4};
int[] value = {15, 20, 30};
int bagsize = 4;
testweightbagproblem(weight, value, bagsize);
}
public static void testweightbagproblem(int[] weight, int[] value, int bagsize){
int wlen = weight.length, value0 = 0;
//定义dp数组:dp[i][j]表示背包容量为j时,前i个物品能获得的最大价值
int[][] dp = new int[wlen + 1][bagsize + 1];
//初始化:背包容量为0时,能获得的价值都为0
for (int i = 0; i <= wlen; i++){
dp[i][0] = value0;
}
//遍历顺序:先遍历物品,再遍历背包容量
for (int i = 1; i <= wlen; i++){
for (int j = 1; j <= bagsize; j++){
if (j < weight[i - 1]){
dp[i][j] = dp[i - 1][j];
}else{
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i - 1]] + value[i - 1]);
}
}
}
//打印dp数组
for (int i = 0; i <= wlen; i++){
for (int j = 0; j <= bagsize; j++){
System.out.print(dp[i][j] + " ");
}
System.out.print("\n");
}
}
华为机试-HJ16 购物单
题目
题目链接:购物单
这是我做的第一道关于背包问题的笔试题。对于刚接触动态规划的我来说真的是噩梦。看着别人的答案我都看不懂,网上找了不少的题解,也没有详细说明为什么要这么做,当然也是我对于背包问题的理解不够深。在我弄懂以后我就想写一写关于这道噩梦的解题思路。这种题也是中等难度的?我tui
大佬的思路讲解
我就是看了这篇文章后才搞明白这道题的
王强决定把年终奖用于购物,他把想买的物品分为两类:主件与附件,附件是从属于某个主件的,下表就是一些主件与附件的例子:
主件 | 附件 |
---|---|
电脑 | 打印机,扫描仪 |
书柜 | 图书 |
书桌 | 台灯,文具 |
工作椅 | 无 |
如果要买归类为附件的物品,必须先买该附件所属的主件,且每件物品只能购买一次。
每个主件可以有 0 个、 1 个或 2 个附件。附件不再有从属于自己的附件。请你帮助王强计算可获得的最大的满意度。
输入:
1000 5
800 2 0
400 5 1
300 5 1
400 3 0
500 2 0
输出:2200
简单概括一下这道题。输入拥有的钱和物品的数量,然后输入物品的价格,重要度,附件编号。让你计算最大的满意度(满意度=价格×重要度)。
转换一下:拥有的钱=背包的重量,满意度=最大价值总和,物品的价格=物品的重量。
难点在于,这个题目中增加了附件,比如例子中的价值400,重要度5的物品,是物件1的附件,只能买了物品1以后才能买物品2。同理3也是1的附件。附件编号为0的为主件,没有购买前置条件。
解题过程
施工中
后续再有比较难的背包问题也会继续记录
参考资料
代码随想录:里面有很多算法的思路和例子