mirror of
https://github.com/youngyangyang04/leetcode-master.git
synced 2025-07-05 22:59:31 +08:00
Update
This commit is contained in:
@ -1,103 +0,0 @@
|
||||
# 什么是动态规划
|
||||
|
||||
动态规划,英文:Dynamic Programming,简称DP,如果某一问题有很多重叠子问题,使用动态规划是最有效的。
|
||||
|
||||
所以动态规划中每一个状态一定是由上一个状态推导出来的,**这一点就区分于贪心**,贪心是局部直接选最优的,
|
||||
|
||||
在[关于贪心算法,你该了解这些!](https://mp.weixin.qq.com/s/O935TaoHE9Eexwe_vSbRAg)中我举了一个背包问题的例子。
|
||||
|
||||
例如:有N件物品和一个最多能被重量为W 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。**每件物品只能用一次**,求解将哪些物品装入背包里物品价值总和最大。
|
||||
|
||||
动态规划中dp[j]是又dp[j-weight[i]]推导出来的。
|
||||
|
||||
但如果是贪心呢,每次拿物品选一个最大的或者最小的就完事了。
|
||||
|
||||
所以贪心解决不了动态规划的问题,这也是最大的区别。
|
||||
|
||||
大家也不用死扣动规和贪心的理论区别,后面做做题目自然就知道了。
|
||||
|
||||
而且很多讲解动态规划的文章都会讲最优子结构啊和重叠子问题啊这些,这些东西都是教科书的上定义,晦涩难懂而且不实用。
|
||||
|
||||
大家知道动规是由前一个状态推导出来的,而贪心是局部直接选最优的,对于刷题来说就够用了。
|
||||
|
||||
上述提到的背包问题,后序会详细讲解。
|
||||
|
||||
# 动态规划的解题步骤
|
||||
|
||||
题目的时候,很多同学会陷入一个误区,就是以为把状态转移公式背下来,照葫芦画瓢改改,就开始写代码,甚至把题目AC之后,都不太清楚dp[i]表示的是什么。
|
||||
|
||||
这就是一种朦胧的状态,然后就把题给过了,遇到稍稍难一点的,可能直接就不会了,然后看题解,然后继续照葫芦画瓢陷入这种恶性循环中。
|
||||
|
||||
状态转移公式(递推公式)是很重要,但动规不仅仅只有递推公式。
|
||||
|
||||
**对于动态规划问题,我将拆解为如下五步曲,这四步都搞清楚了,才能说把动态规划真的掌握了!**
|
||||
|
||||
* 确定dp数组以及下标的含义
|
||||
* 确定递推公式
|
||||
* dp数组如何初始化
|
||||
* 确定遍历顺序
|
||||
* 举例推导dp数组
|
||||
|
||||
一些同学可能想为什么要先确定递推公式,然后在考虑初始化呢?
|
||||
|
||||
因为一些情况是递推公式决定了dp数组要输入初始化!
|
||||
|
||||
后面的讲解中我都是围绕着这四个点来经行讲解。
|
||||
|
||||
可能刷过动态规划题目的同学可能都知道递推公式的重要性,感觉确定了递推公式这道题目就解出来了。
|
||||
|
||||
其实 确定递推公式 仅仅是解题里的一步而且,dp数组以及下标的含义, dp数组的初始化 以及确定遍历顺序,都非常重要。
|
||||
|
||||
后序的讲解的大家就会发现其重要性。
|
||||
|
||||
很多同学甚至知道递推公式,但搞不清楚dp数组应该如何初始化,或者正确的遍历顺序,以至于记下来公式,但写的程序怎么改都通过不了。
|
||||
|
||||
# 动态规划应该如何debug
|
||||
|
||||
平时我自己写的时候也经常出问题,**找问题的最好方式就是把dp数组打印出来,看看究竟是不是按照自己思路推导的!**
|
||||
|
||||
一些同学对于dp的学习是上来就是俯视类型的,总是想一下子全盘接纳,一鼓作气写出代码,如果代码能通过万事大吉,通过不了的话就凭感觉改一改。
|
||||
|
||||
这是一个很不好的习惯!
|
||||
|
||||
**做动规的题目,写代码之前一定要把状态转移在dp数组的上具体情况模拟一遍,心中有数,确定最后推出的是想要的结果**。
|
||||
|
||||
然后在写代码,如果代码没通过就打印dp数组,看看是不是和自己推导的哪里不一样。
|
||||
|
||||
如果和自己模拟推导的一样,那么就是自己的递归公式有问题。
|
||||
|
||||
如果和自己模拟推导的不一样,那么就是代码实现细节有问题。
|
||||
|
||||
**这样才是一个完整的思考过程,而不是一旦代码出问题,就毫无头绪的东改改西改改,最后过不了,或者说是稀里糊涂的过了**。
|
||||
|
||||
这也是我为什么在动规五步曲里强调距离推导dp数组的重要性。
|
||||
|
||||
举个例子:一些同学可能代码通过不了,都会把代码抛到出来问:我这里代码都已经和题解一模一样的,为什么通过不了呢?
|
||||
|
||||
发出这样的问题之前,其实可以自己先思考这三个问题:
|
||||
|
||||
* 这道题目我举例推导状态转移公式了么?
|
||||
* 我打印dp数组的日志了么?
|
||||
* 打印出来了dp数组和我想的一样么?
|
||||
|
||||
**如果这灵魂三问自己都做到了,基本上这道题目也就解决了**,或者更清晰的知道自己究竟是哪一点不明白,是状态转移不明白,还是实现代码不知道该怎么写,还是不理解遍历dp数组的顺序。
|
||||
|
||||
然后在问问题,目的性就很强了,回答问题的同学也可以快速知道提问者的疑惑了。
|
||||
|
||||
|
||||
# 总结
|
||||
|
||||
这一篇是动态规划的整体概述,讲解了什么是动态规划,动态规划的解题步骤,以及如何debug。
|
||||
|
||||
动态规划是一个很大的领域,今天这一篇讲解的内容是整个动态规划系列里都会使用到的一些理论基础。
|
||||
|
||||
在后序讲解中针对某一具体问题,还会讲解其对应的理论基础,例如背包问题中的01背包,leetcode上的题目都是01背包的应用,而没有纯01背包的问题,那么就需要在把对应的理论知识讲解一下。
|
||||
|
||||
一些同学可能着急想刷题,这个我很理解,我写的理论基础篇已经是非常偏实用的了,还是需要一点基础的。新加入的录友可能不了解,可以在「算法汇总」中看到每一个系列开始的时候都有对应的理论基础篇,都是特别实用的理论基础了。
|
||||
|
||||
今天我们开始新的征程了,你准备好了么?
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -1,22 +0,0 @@
|
||||
|
||||
<img src='../pics/416.分割等和子集1.png' width=600> </img></div>
|
||||
|
||||
组合问题公式:dp[i] += dp[i-num]
|
||||
True、False问题公式:dp[i] = dp[i] or dp[i-num]
|
||||
最大最小问题公式:dp[i] = min(dp[i], dp[i-num]+1)或者dp[i] = max(dp[i], dp[i-num]+1)
|
||||
|
||||
|
||||
接下来讲一下背包问题的判定
|
||||
背包问题具备的特征:给定一个target,target可以是数字也可以是字符串,再给定一个数组nums,nums中装的可能是数字,也可能是字符串,问:能否使用nums中的元素做各种排列组合得到target。
|
||||
|
||||
背包问题技巧:
|
||||
1.如果是0-1背包,即数组中的元素不可重复使用,nums放在外循环,target在内循环,且内循环倒序;
|
||||
for num in nums:
|
||||
for i in range(target, nums-1, -1):
|
||||
2.如果是完全背包,即数组中的元素可重复使用,nums放在外循环,target在内循环。且内循环正序。
|
||||
for num in nums:
|
||||
for i in range(nums, target+1):
|
||||
3.如果组合问题需考虑元素之间的顺序,需将target放在外循环,将nums放在内循环。
|
||||
for i in range(1, target+1):
|
||||
for num in nums:
|
||||
|
@ -1,240 +0,0 @@
|
||||
|
||||
leetcode上没有纯01背包的问题,都是需要转化为01背包的题目,所以我先把通过纯01背包问题,把01背包原理讲清楚,后序讲解leetcode题目的时候,重点就是如何转化为01背包问题了。
|
||||
|
||||
# 01 背包
|
||||
|
||||
有N件物品和一个最多能被重量为W 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。**每件物品只能用一次**,求解将哪些物品装入背包里物品价值总和最大。
|
||||
|
||||
这是标准的背包问题,以至于很多同学看了这个自然就会想到背包,甚至都不知道暴力的解法应该怎么解。
|
||||
|
||||
这样其实就是没有从底向上去思考,而是习惯性的只知道背包了,那么暴力的解法应该是怎么样的呢?
|
||||
|
||||
每一件物品其实只有两个状态,取或者不取,所以可以使用回溯法搜索出所有的情况,那么时间复杂度就是O(2^n),这里的n表示物品数量。
|
||||
|
||||
所以暴力的解法是指数级别的时间复杂度。进而才需要动态规划的解法来进行优化!
|
||||
|
||||
目前leetcode上没有发现有纯01背包的题目,leetcode上相关01背包问题都是需要某种条件转化为01背包问题,所以 我举一个纯01背包的例子来给大家讲解。
|
||||
|
||||
把01背包理论和代码理解透彻了,我们再刷leetcode上的题目。
|
||||
|
||||
下面的讲解中,我举一个例子:
|
||||
|
||||
背包最大重量为4。
|
||||
|
||||
物品为:
|
||||
|
||||
| | 重量 | 价值 |
|
||||
| --- | --- | --- |
|
||||
| 物品0 | 1 | 15 |
|
||||
| 物品1 | 3 | 20 |
|
||||
| 物品2 | 4 | 30 |
|
||||
|
||||
以下讲解和图示中出现的数字都是以这个例子为例。
|
||||
|
||||
## 确定dp数组以及下标的含义
|
||||
|
||||
对于背包问题,有一种写法, 是使用二维数组,即**dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少**。
|
||||
|
||||
只看这个二维数组的定义,大家一定会有点懵,看下面这个图:
|
||||
|
||||
<img src='../pics/动态规划-背包问题1.png' width=600> </img></div>
|
||||
|
||||
**要时刻记着这个dp数组的含义,下面的一些步骤都围绕这dp数组的含义进行的**,如果哪里看懵了,就来回顾一下i代表什么,j又代表什么。
|
||||
|
||||
## 确定递推公式
|
||||
|
||||
再回顾一下dp[i][j]的含义:从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。
|
||||
|
||||
那么可以有两个方向推出来dp[i][j],
|
||||
|
||||
* 由dp[i - 1][j]推出,即背包里不放物品i的最大价值,此时dp[i][j]就是dp[i - 1][j]
|
||||
* 由dp[i - 1][j - weight[i]]推出,dp[i - 1][j - weight[i]] 为背包容量为j - weight[i]的时候不放物品i的最大价值,那么dp[i - 1][j - weight[i]] + value[i] (物品i的价值),就是背包放物品i得到的最大价值
|
||||
|
||||
所以递归公式: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
|
||||
|
||||
## dp数组如何初始化
|
||||
|
||||
**关于初始化,一定要和dp数组的定义吻合,否则到递推公式的时候就会越来越乱**。
|
||||
|
||||
首先从dp[i][j]的定义触发,如果背包容量j为0的话,无论是选取哪些物品,背包价值总和一定为0。如图:
|
||||
|
||||
<img src='../pics/动态规划-背包问题2.png' width=600> </img></div>
|
||||
|
||||
|
||||
那么其他下标应该初始化多少呢?
|
||||
|
||||
dp[i][j]在推导的时候一定是取价值最大的数,如果题目给的价值都是正整数那么非0下标都初始化为0就可以了,因为0就是最小的了,不会影响去最大价值的结果。
|
||||
|
||||
如果题目给的价值有负数,那么非0下标就要初始化为负无穷了。例如:一个物品的价值是-2,但对应的位置依然初始化为0,那么去最大值的时候,就会取0而不是-2了,所以要初始化为负无穷。
|
||||
|
||||
这样才能让dp数组在递归公式的过程中取的最大的价值,而不是被初始值覆盖了。
|
||||
|
||||
而本题价值都是正整数,所以初始化为0就可以了。
|
||||
|
||||
如图:
|
||||
|
||||
<img src='../pics/动态规划-背包问题3.png' width=600> </img></div>
|
||||
|
||||
**很明显,红框的位置就是我们要求的结果**
|
||||
|
||||
|
||||
## 确定遍历顺序
|
||||
|
||||
确定递归公式之后,还要确定遍历顺序。
|
||||
|
||||
在如下图中,可以看出,有两个遍历的维度:物品与背包重量
|
||||
<img src='../pics/动态规划-背包问题3.png' width=600> </img></div>
|
||||
|
||||
那么问题来了,先遍历 物品还是先遍历背包重量呢?
|
||||
|
||||
**其实都可以!! 但是先遍历物品更好理解**。下面讲到具体原因的时候来在分析原因。
|
||||
|
||||
那么首先遍历物品,然后遍历背包重量。
|
||||
|
||||
注意 状态转移方程 dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); 中有两个下标为负数的情况,即:i - 1 和 j - weight[i]。
|
||||
|
||||
既然i 是由 i-1 推导出来,那么i为0的时候就一定要初始化,i为0,存放编号0的物品的时候,各个容量的背包所能存放的最大价值。
|
||||
|
||||
代码如下:
|
||||
|
||||
```
|
||||
// 倒叙遍历
|
||||
for (int j = bagWeight; j >= weight[0]; j--) {
|
||||
dp[0][j] = dp[0][j - weight[0]] + value[0]; // 初始化i为0时候的情况
|
||||
}
|
||||
```
|
||||
|
||||
**大家应该发现,这个初始化为什么是倒叙的遍历的?正序遍历就不行么?**
|
||||
|
||||
正序遍历还真就不行,dp[0][j]表示容量为j的背包存放物品0时候的最大价值,物品0的价值就是15,因为题目中说了**每个物品只有一个!**所以dp[0][j]如果不是初始值的话,就应该都是物品0的价值,也就是15。
|
||||
|
||||
但如果一旦正序遍历了,那么物品0就会被重复加入多次! 例如代码如下:
|
||||
```
|
||||
// 正序遍历
|
||||
for (int j = weight[0]; j <= bagWeight; j++) {
|
||||
dp[0][j] = dp[0][j - weight[0]] + value[0];
|
||||
}
|
||||
```
|
||||
|
||||
例如dp[0][1] 是15,到了dp[0][2] = dp[0][2 - 1] + 15; 也就是dp[0][2] = 30 了,那么就是物品0被重复放入了。
|
||||
|
||||
**所以一定要倒叙遍历,保证物品0只被放入一次!这一点对01背包很重要,后面在讲解滚动数组的时候,还会用到倒叙遍历来保证物品使用一次!**
|
||||
|
||||
|
||||
## 举例推导dp数组
|
||||
|
||||
dp[][] = dp[][] **应该这样手动推动一下**
|
||||
|
||||
来看一下对应的dp数组的数值,如图:
|
||||
|
||||
<img src='../pics/动态规划-背包问题4.png' width=600> </img></div>
|
||||
|
||||
最终结果就是dp[2][4]。
|
||||
|
||||
建议大家此时自己在纸上推导一遍,看看dp数组里每一个数值是不是这样的。
|
||||
|
||||
**做动态规划的题目,最好的过程就是自己在纸上举一个例子把对应的dp数组的数值推导一下,然后在动手写代码!**
|
||||
|
||||
很多同学做dp题目,遇到各种问题,然后凭感觉东改改西改改,怎么改都不对,或者稀里糊涂就改过了。
|
||||
|
||||
主要就是自己没有动手推导一下dp数组的演变过程,如果推导明白了,代码写出来就算有问题,只要把dp数组打印出来,对比一下和自己推导的有什么差异,很快就可以发现问题了。
|
||||
|
||||
## 遍历过程代码
|
||||
|
||||
初始化dp数组之后,就可以先遍历物品,在遍历背包,然后使用公式推导了,代码如下:
|
||||
|
||||
```
|
||||
// 遍历过程
|
||||
for(int i = 1; i < weight.size(); i++) { // 遍历物品
|
||||
for(int j = 0; j <= bagWeight; j++) { // 遍历背包重量
|
||||
if (j < weight[i]) dp[i][j] = dp[i - 1][j];
|
||||
else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
|
||||
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
遍历过程的代码其实优化的,我是为了把dp数组里数值完整表现出来,精简一下可以是:
|
||||
|
||||
```
|
||||
// 遍历过程
|
||||
for(int i = 1; i < weight.size(); i++) { // 遍历物品
|
||||
for(int j = 0; j <= bagWeight; j++) { // 遍历背包重量
|
||||
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 完整代码
|
||||
|
||||
```C++
|
||||
void 01bagProblem() {
|
||||
int w[] = {1, 3, 4};
|
||||
int v[] = {15, 20, 30};
|
||||
int bagWeight = 4;
|
||||
|
||||
vector<int> weight(w, w + sizeof(w)/sizeof(int));
|
||||
vector<int> value(v, v + sizeof(v)/sizeof(int));
|
||||
|
||||
vector<vector<int>> dp(weight.size() + 1, vector<int>(bagWeight + 1, 0));
|
||||
|
||||
for (int j = bagWeight; j >= weight[0]; j--) {
|
||||
dp[0][j] = dp[0][j - weight[0]] + value[0];
|
||||
}
|
||||
// weight数组的大小 就是物品个数
|
||||
for(int i = 1; i < weight.size(); i++) { // 遍历物品
|
||||
for(int j = 0; j <= bagWeight; j++) { // 遍历背包容量
|
||||
if (j < weight[i]) dp[i][j] = dp[i - 1][j]; // 这个是为了展现dp数组里元素的变化,可以删掉
|
||||
else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
|
||||
|
||||
}
|
||||
}
|
||||
// 把dp数组打印出来,看看对不对
|
||||
for (int i = 0 ; i < weight.size(); i++) {
|
||||
for (int j = 0; j <= bagWeight; j++) {
|
||||
cout << dp[i][j] << " ";
|
||||
}
|
||||
cout << endl;
|
||||
}
|
||||
// 输出结果
|
||||
cout << dp[weight.size() - 1][bagWeight] << endl;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
上面我们讲到 两层for循环的顺序,讲解的过程是用先遍历物品,在遍历背包。
|
||||
|
||||
那么先遍历背包,再遍历物品,也是可以的!
|
||||
|
||||
例如这样:
|
||||
|
||||
```
|
||||
// weight数组的大小 就是物品个数
|
||||
for(int j = 0; j <= bagWeight; j++) { // 遍历背包容量
|
||||
for(int i = 1; i < weight.size(); i++) { // 遍历物品
|
||||
if (j < weight[i]) dp[i][j] = dp[i - 1][j];
|
||||
else dp[i][j] = max(dp[i - 1][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]是靠dp[i-1][j]和dp[i - 1][j - weight[i]]推导出来的。
|
||||
|
||||
dp[i-1][j]和dp[i - 1][j - weight[i]] 都在dp[i][j]的左上角方向(包括正左和正上两个方向),那么先遍历物品,在遍历背包的过程如图所示:
|
||||
|
||||
|
||||
<img src='../pics/动态规划-背包问题5.png' width=600> </img></div>
|
||||
|
||||
在来看看先遍历背包,再遍历物品呢,如图:
|
||||
|
||||
<img src='../pics/动态规划-背包问题6.png' width=600> </img></div>
|
||||
|
||||
**大家可以看出,虽然两个for循环遍历的次序不同,但是dp[i][j]所需要的数据就是左上角,根本不影响dp[i][j]公式的推导!**
|
||||
|
||||
这一点其实很多题解都没有说清楚,此时大家应该对01背包,两层for控制两个维度,究竟先遍历哪一个有一个深刻理解了。
|
||||
|
||||
|
@ -1,135 +0,0 @@
|
||||
|
||||
## 滚动数组
|
||||
|
||||
对于背包问题其实状态都是可以压缩的。
|
||||
|
||||
在使用二维数组的时候,递推公式:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
|
||||
|
||||
这里其实可以发现如果把dp[i-1]那一层拷贝到dp[i]上,表达式完全可以是:dp[i][j] = max(dp[i][j], dp[i][j - weight[i]] + value[i]);
|
||||
|
||||
读到这里估计大家都忘了 dp[i][j]里的i和j表达的是什么了,i是物品,j是背包容量,一定要时刻记住这里i和j的含义,要不然很容易看懵了。
|
||||
|
||||
**与其把dp[i-1]这一层拷贝到dp[i]上,不如只用一个一维数组了**。
|
||||
|
||||
只用dp[j]!(一维数组,也可以理解是一个滚动数组)
|
||||
|
||||
在一维dp数组中,dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j]
|
||||
|
||||
* 一维dp数组如何初始化
|
||||
|
||||
**关于初始化,一定要和dp数组的定义吻合,否则到递推公式的时候就会越来越乱**。
|
||||
|
||||
dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j],那么dp[0]就应该是0,因为背包容量为0所背的物品的最大价值就是0。
|
||||
|
||||
那么dp数组除了下标0的位置,初始为0,其他下标应该初始化多少呢?
|
||||
|
||||
在回顾一下dp数组的含义:容量为j的背包,所背的物品价值可以最大为dp[j]。
|
||||
|
||||
那么dp数组在推导的时候一定是取价值最大的数,如果题目给的价值都是正整数那么非0下标都初始化为0就可以了,如果题目给的价值有负数,那么非0下标就要初始化为负无穷。
|
||||
|
||||
**这样才能让dp数组在递归公式的过程中取的最大的价值,而不是被初始值覆盖了**。
|
||||
|
||||
那么我假设物品价值都是大于0的,所以dp数组初始化的时候,都初始为0就可以了。
|
||||
|
||||
|
||||
* 一维dp数组的递推公式
|
||||
|
||||
dp[j]为 容量为j的背包所背的最大价值,那么如何推导dp[j]呢?
|
||||
|
||||
dp[j]可以通过dp[j - weight[j]]推导出来,dp[j - weight[i]]表示容量为j - weight[i]的背包所背的最大价值。
|
||||
|
||||
dp[j - weight[i]] + value[i] 表示 容量为 j - 物品i体积 的背包 加上 物品i的价值。
|
||||
|
||||
那么最大的dp[j]可能就是 dp[j - weight[i]] + value[i]。
|
||||
|
||||
那么此时dp[j]有两个选择,一个是取自己dp[j],一个是取dp[j - weight[i]] + value[i],指定是取最大的,毕竟是求最大价值,
|
||||
|
||||
所以递归公式为:
|
||||
|
||||
```
|
||||
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
|
||||
```
|
||||
|
||||
可以看出相对于二维dp数组的写法,就是把dp[i][j]中i的维度去掉了。
|
||||
|
||||
* 一维dp数组遍历顺序
|
||||
|
||||
代码如下:
|
||||
|
||||
```
|
||||
for(int i = 0; i < weight.size(); i++) { // 遍历物品
|
||||
for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
|
||||
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
|
||||
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**这里大家发现和二维dp的写法中,遍历背包的顺序是不一样的!**
|
||||
|
||||
二维dp遍历的时候,背包容量是从小到大,而一维dp遍历的时候,背包是从大到小。
|
||||
|
||||
为什么呢?
|
||||
|
||||
**倒叙遍历是为了保证物品i只被放入一次!**,在上面讲解二维dp遍历第0行的时候已经提到过一次。
|
||||
|
||||
举一个例子:物品0的重量weight[0] = 1,价值value[0] = 15
|
||||
|
||||
如果正序遍历
|
||||
|
||||
dp[1] = dp[1 - weight[0]] + value[0] = 15
|
||||
|
||||
dp[2] = dp[2 - weight[0]] + value[0] = 30
|
||||
|
||||
此时dp[2]就已经是30了,意味着物品0,被放入了两次,所以不能正序遍历。
|
||||
|
||||
为什么倒叙遍历,就可以保证物品只放入一次呢?
|
||||
|
||||
倒叙就是先算dp[2]
|
||||
|
||||
dp[2] = dp[2 - weight[0]] + value[0] = 15 (dp数组已经都初始化为0)
|
||||
|
||||
dp[1] = dp[1 - weight[0]] + value[0] = 15
|
||||
|
||||
所以从后往前循环,每次取得状态不会和之前取得状态重合,这样每种物品就只取一次了。
|
||||
|
||||
|
||||
**那么问题又来了,为什么二维dp遍历的时候不用倒叙呢?**
|
||||
|
||||
因为对数二维dp,dp[i][j]都是通过上一层即dp[i-1][j]计算而来,不用考虑重复计算dp[i][j]的问题!
|
||||
|
||||
|
||||
再来看看两个嵌套for循环的顺序,代码中是先遍历物品嵌套遍历背包容量,那可不可以先背包容量嵌套遍历物品呢?
|
||||
|
||||
不可以!
|
||||
|
||||
因为一维dp的写法,背包容量一定是要倒序遍历(原因上面已经讲了),如果遍历背包容量放在上一层,dp[bagWeight]只会放入一个物品。
|
||||
|
||||
一维dp01背包完整测试代码如下:
|
||||
|
||||
```
|
||||
void test_1_wei_bag_problem() {
|
||||
int w[] = {1, 3, 4};
|
||||
int v[] = {15, 20, 30};
|
||||
int bagWeight = 4;
|
||||
vector<int> weight(w, w + sizeof(w)/sizeof(int));
|
||||
vector<int> value(v, v + sizeof(v)/sizeof(int));
|
||||
int dp[6] = {0};
|
||||
for(int i = 0; i < weight.size(); i++) { // 遍历物品
|
||||
for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
|
||||
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
|
||||
}
|
||||
// 把dp数组打印出来,看看对不对
|
||||
for (int j = 0; j <= bagWeight; j++) {
|
||||
cout << dp[j] << " ";
|
||||
}
|
||||
cout << endl;
|
||||
}
|
||||
// 输出结果
|
||||
cout << dp[bagWeight] << endl;
|
||||
}
|
||||
|
||||
```
|
||||
# 总结
|
||||
|
||||
我个人倾向于使用一维dp数组的写法,比较直观简洁,所以在后面背包问题的讲解中,我都直接使用一维dp数组来进行推导。
|
@ -1,97 +0,0 @@
|
||||
|
||||
# 背包问题
|
||||
|
||||
背包九讲其实看起来还是有点费劲的,而且都是伪代码理解起来吃力
|
||||
|
||||
# 完全背包
|
||||
|
||||
有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。**每件物品都有无限个(也就是可以放入背包多次)**,求解将哪些物品装入背包里物品价值总和最大。
|
||||
|
||||
|
||||
完全背包和01背包问题唯一不同的地方就是,每种物品有无限件。
|
||||
|
||||
关于完全背包和01背包的差别还是好好好讲一讲。
|
||||
|
||||
同样因为leetcode上没有纯完全背包问题,都是需要完全背包的各种应用,需要转化成完全背包问题,所以我这里还是以纯完全背包问题进行讲解理论和原理。讲leetcode题目的时候则侧重于讲如何转化为完全背包问题。
|
||||
|
||||
|
||||
首先在回顾一下01背包的核心代码
|
||||
|
||||
```
|
||||
for(int i = 0; i < weight.size(); i++) { // 遍历物品
|
||||
for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
|
||||
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
|
||||
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
我们知道01背包内嵌的循环是从大到小遍历,为了保证每个物品仅被添加一次。
|
||||
|
||||
|
||||
而完全背包的物品是可以添加多次的,所以要从小打到遍历,即:
|
||||
|
||||
```
|
||||
for(int i = 0; i < weight.size(); i++) { // 遍历物品
|
||||
for(int j = weight[i]; j < bagWeight ; j++) { // 遍历背包容量
|
||||
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
|
||||
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
在讲解01背包中,已经讲解了为什么正序遍历物品就可以添加多次。
|
||||
|
||||
相信很多同学看网上的文章,关于完全背包介绍基本就到为止了。
|
||||
|
||||
其实还有一个很重要的问题,为什么遍历物品在外层循环,遍历背包容量在内层循环?
|
||||
|
||||
这个问题很多题解都避而不谈,大家都默认 遍历物品在外层,遍历背包容量在内层,好像本应该如此一样,那么为什么呢?
|
||||
|
||||
难道就不能,遍历背包容量在外层,遍历物品在内层?
|
||||
|
||||
接下来我们好好分析一下,让大家学个通透!
|
||||
|
||||
|
||||
|
||||
|
||||
把对n的历遍放到第一层循环,这样才能避免把[1,5]、[5,1]算作两条路径。因为你限制了1,5的顺序,
|
||||
|
||||
到了i=5之后不可能在发生5,1的情况产生。
|
||||
|
||||
对于方式二,把对n的历遍放在第二层,对于任意的一个状态v,都可能历遍每一种硬币,会导致重复冗余的问题。
|
||||
|
||||
如果想加深理解,建议最好把两种方式都实现一下,单步执行查看
|
||||
|
||||
|
||||
|
||||
背包问题技巧:
|
||||
1.如果是0-1背包,即数组中的元素不可重复使用,nums放在外循环,target在内循环,且内循环倒序;
|
||||
for num in nums:
|
||||
for i in range(target, nums-1, -1):
|
||||
2.如果是完全背包,即数组中的元素可重复使用,nums放在外循环,target在内循环。且内循环正序。
|
||||
for num in nums:
|
||||
for i in range(nums, target+1):
|
||||
3.如果组合问题需考虑元素之间的顺序,需将target放在外循环,将nums放在内循环。
|
||||
for i in range(1, target+1):
|
||||
for num in nums:
|
||||
|
||||
|
||||
|
||||
值得一提的是,上面的伪代码中两层for循环的次序可以颠倒。这个结论有可能会带来算法时间常数上的优化。(可能说的就是组合或者排列了)
|
||||
|
||||
# 多重背包
|
||||
|
||||
有N种物品和一个容量为V 的背包。第i种物品最多有Mi件可用,每件耗费 的空间是Ci ,价值是Wi 。求解将哪些物品装入背包可使这些物品的耗费的空间 总和不超过背包容量,且价值总和最大。
|
||||
|
||||
这题目和完全背包问题很类似。基本的方程只需将完全背包问题的方程略 微一改即可。
|
||||
|
||||
与 0-1 背包的区别在于每种物品 y 有 k 个,而非 1 个
|
||||
|
||||
力扣上面没有多重背包的题目。
|
||||
# 总结
|
||||
|
||||
|
||||
# 总结
|
||||
|
||||
后台回复:背包九讲 就可以获得pdf
|
Reference in New Issue
Block a user