-
Notifications
You must be signed in to change notification settings - Fork 23.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
c967627
commit e94b84c
Showing
5 changed files
with
646 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,160 @@ | ||
# 动态规划之背包问题 | ||
|
||
|
||
<p align='center'> | ||
<a href="https://github.com/labuladong/fucking-algorithm" target="view_window"><img alt="GitHub" src="https://img.shields.io/github/stars/labuladong/fucking-algorithm?label=Stars&style=flat-square&logo=GitHub"></a> | ||
<a href="https://www.zhihu.com/people/labuladong"><img src="https://img.shields.io/badge/%E7%9F%A5%E4%B9%[email protected]?style=flat-square&logo=Zhihu"></a> | ||
<a href="https://i.loli.net/2020/10/10/MhRTyUKfXZOlQYN.jpg"><img src="https://img.shields.io/badge/公众号[email protected]?style=flat-square&logo=WeChat"></a> | ||
<a href="https://space.bilibili.com/14089380"><img src="https://img.shields.io/badge/B站[email protected]?style=flat-square&logo=Bilibili"></a> | ||
</p> | ||
|
||
![](../pictures/souyisou.png) | ||
|
||
相关推荐: | ||
* [经典动态规划:最长公共子序列](https://labuladong.gitee.io/algo/) | ||
* [特殊数据结构:单调栈](https://labuladong.gitee.io/algo/) | ||
|
||
**-----------** | ||
|
||
本文有视频版:[0-1背包问题详解](https://www.bilibili.com/video/BV15B4y1P7X7/) | ||
|
||
后台天天有人问背包问题,这个问题其实不难啊,如果我们号动态规划系列的十几篇文章你都看过,借助框架,遇到背包问题可以说是手到擒来好吧。无非就是状态 + 选择,也没啥特别之处嘛。 | ||
|
||
今天就来说一下背包问题吧,就讨论最常说的 0-1 背包问题。描述: | ||
|
||
给你一个可装载重量为 `W` 的背包和 `N` 个物品,每个物品有重量和价值两个属性。其中第 `i` 个物品的重量为 `wt[i]`,价值为 `val[i]`,现在让你用这个背包装物品,最多能装的价值是多少? | ||
|
||
举个简单的例子,输入如下: | ||
|
||
``` | ||
N = 3, W = 4 | ||
wt = [2, 1, 3] | ||
val = [4, 2, 3] | ||
``` | ||
|
||
算法返回 6,选择前两件物品装进背包,总重量 3 小于 `W`,可以获得最大价值 6。 | ||
|
||
题目就是这么简单,一个典型的动态规划问题。这个题目中的物品不可以分割,要么装进包里,要么不装,不能说切成两块装一半。这就是 0-1 背包这个名词的来历。 | ||
|
||
解决这个问题没有什么排序之类巧妙的方法,只能穷举所有可能,根据我们 [动态规划详解](https://labuladong.gitee.io/algo/) 中的套路,直接走流程就行了。 | ||
|
||
### 动规标准套路 | ||
|
||
看来我得每篇动态规划文章都得重复一遍套路,历史文章中的动态规划问题都是按照下面的套路来的。 | ||
|
||
**第一步要明确两点,「状态」和「选择」**。 | ||
|
||
先说状态,如何才能描述一个问题局面?只要给几个物品和一个背包的容量限制,就形成了一个背包问题呀。**所以状态有两个,就是「背包的容量」和「可选择的物品」**。 | ||
|
||
再说选择,也很容易想到啊,对于每件物品,你能选择什么?**选择就是「装进背包」或者「不装进背包」嘛**。 | ||
|
||
明白了状态和选择,动态规划问题基本上就解决了,只要往这个框架套就完事儿了: | ||
|
||
```python | ||
for 状态1 in 状态1的所有取值: | ||
for 状态2 in 状态2的所有取值: | ||
for ... | ||
dp[状态1][状态2][...] = 择优(选择1,选择2...) | ||
``` | ||
|
||
PS:此框架出自历史文章 [团灭 LeetCode 股票问题](https://labuladong.gitee.io/algo/)。 | ||
|
||
**第二步要明确 `dp` 数组的定义**。 | ||
|
||
首先看看刚才找到的「状态」,有两个,也就是说我们需要一个二维 `dp` 数组。 | ||
|
||
`dp[i][w]` 的定义如下:对于前 `i` 个物品,当前背包的容量为 `w`,这种情况下可以装的最大价值是 `dp[i][w]`。 | ||
|
||
比如说,如果 `dp[3][5] = 6`,其含义为:对于给定的一系列物品中,若只对前 3 个物品进行选择,当背包容量为 5 时,最多可以装下的价值为 6。 | ||
|
||
PS:为什么要这么定义?便于状态转移,或者说这就是套路,记下来就行了。建议看一下我们的动态规划系列文章,几种套路都被扒得清清楚楚了。 | ||
|
||
根据这个定义,我们想求的最终答案就是 `dp[N][W]`。base case 就是 `dp[0][..] = dp[..][0] = 0`,因为没有物品或者背包没有空间的时候,能装的最大价值就是 0。 | ||
|
||
细化上面的框架: | ||
|
||
```python | ||
int[][] dp[N+1][W+1] | ||
dp[0][..] = 0 | ||
dp[..][0] = 0 | ||
|
||
for i in [1..N]: | ||
for w in [1..W]: | ||
dp[i][w] = max( | ||
把物品 i 装进背包, | ||
不把物品 i 装进背包 | ||
) | ||
return dp[N][W] | ||
``` | ||
|
||
**第三步,根据「选择」,思考状态转移的逻辑**。 | ||
|
||
简单说就是,上面伪码中「把物品 `i` 装进背包」和「不把物品 `i` 装进背包」怎么用代码体现出来呢? | ||
|
||
这就要结合对 `dp` 数组的定义,看看这两种选择会对状态产生什么影响: | ||
|
||
先重申一下刚才我们的 `dp` 数组的定义: | ||
|
||
`dp[i][w]` 表示:对于前 `i` 个物品,当前背包的容量为 `w` 时,这种情况下可以装下的最大价值是 `dp[i][w]`。 | ||
|
||
**如果你没有把这第 `i` 个物品装入背包**,那么很显然,最大价值 `dp[i][w]` 应该等于 `dp[i-1][w]`,继承之前的结果。 | ||
|
||
**如果你把这第 `i` 个物品装入了背包**,那么 `dp[i][w]` 应该等于 `dp[i-1][w - wt[i-1]] + val[i-1]`。 | ||
|
||
首先,由于 `i` 是从 1 开始的,所以 `val` 和 `wt` 的索引是 `i-1` 时表示第 `i` 个物品的价值和重量。 | ||
|
||
而 `dp[i-1][w - wt[i-1]]` 也很好理解:你如果装了第 `i` 个物品,就要寻求剩余重量 `w - wt[i-1]` 限制下的最大价值,加上第 `i` 个物品的价值 `val[i-1]`。 | ||
|
||
综上就是两种选择,我们都已经分析完毕,也就是写出来了状态转移方程,可以进一步细化代码: | ||
|
||
```python | ||
for i in [1..N]: | ||
for w in [1..W]: | ||
dp[i][w] = max( | ||
dp[i-1][w], | ||
dp[i-1][w - wt[i-1]] + val[i-1] | ||
) | ||
return dp[N][W] | ||
``` | ||
|
||
**最后一步,把伪码翻译成代码,处理一些边界情况**。 | ||
|
||
我用 C++ 写的代码,把上面的思路完全翻译了一遍,并且处理了 `w - wt[i-1]` 可能小于 0 导致数组索引越界的问题: | ||
|
||
```cpp | ||
int knapsack(int W, int N, vector<int>& wt, vector<int>& val) { | ||
// base case 已初始化 | ||
vector<vector<int>> dp(N + 1, vector<int>(W + 1, 0)); | ||
for (int i = 1; i <= N; i++) { | ||
for (int w = 1; w <= W; w++) { | ||
if (w - wt[i-1] < 0) { | ||
// 这种情况下只能选择不装入背包 | ||
dp[i][w] = dp[i - 1][w]; | ||
} else { | ||
// 装入或者不装入背包,择优 | ||
dp[i][w] = max(dp[i - 1][w - wt[i-1]] + val[i-1], | ||
dp[i - 1][w]); | ||
} | ||
} | ||
} | ||
|
||
return dp[N][W]; | ||
} | ||
``` | ||
|
||
至此,背包问题就解决了,相比而言,我觉得这是比较简单的动态规划问题,因为状态转移的推导比较自然,基本上你明确了 `dp` 数组的定义,就可以理所当然地确定状态转移了。 | ||
|
||
接下来请阅读: | ||
|
||
* [背包问题变体之子集分割](https://labuladong.gitee.io/algo/) | ||
* [完全背包问题之零钱兑换](https://labuladong.gitee.io/algo/) | ||
|
||
**_____________** | ||
|
||
**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitee.io/algo/) 持续更新最新文章**。 | ||
|
||
**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。 | ||
|
||
<p align='center'> | ||
<img src="../pictures/qrcode.jpg" width=200 > | ||
</p> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
# 刷题小技巧 | ||
|
||
|
||
<p align='center'> | ||
<a href="https://github.com/labuladong/fucking-algorithm" target="view_window"><img alt="GitHub" src="https://img.shields.io/github/stars/labuladong/fucking-algorithm?label=Stars&style=flat-square&logo=GitHub"></a> | ||
<a href="https://www.zhihu.com/people/labuladong"><img src="https://img.shields.io/badge/%E7%9F%A5%E4%B9%[email protected]?style=flat-square&logo=Zhihu"></a> | ||
<a href="https://i.loli.net/2020/10/10/MhRTyUKfXZOlQYN.jpg"><img src="https://img.shields.io/badge/公众号[email protected]?style=flat-square&logo=WeChat"></a> | ||
<a href="https://space.bilibili.com/14089380"><img src="https://img.shields.io/badge/B站[email protected]?style=flat-square&logo=Bilibili"></a> | ||
</p> | ||
|
||
![](../pictures/souyisou.png) | ||
|
||
相关推荐: | ||
* [一文解决三道区间问题](https://labuladong.gitee.io/algo/) | ||
* [Union-Find算法详解](https://labuladong.gitee.io/algo/) | ||
|
||
**-----------** | ||
|
||
相信每个人都有过被代码的小 bug 搞得心态爆炸的经历,本文分享一个我最常用的简单技巧,可以大幅提升刷题的幸福感。 | ||
|
||
在这之前,首先回答一个问题,刷力扣题是直接在网页上刷比较好还是在本地 IDE 上刷比较好? | ||
|
||
如果是牛客网笔试那种自己处理输入输出的判题形式,一定要在 IDE 上写,这个没啥说的,但**像力扣这种判题形式,我个人偏好直接在网页上刷**,原因有二: | ||
|
||
**1、方便** | ||
|
||
因为力扣有的数据结构是自定的,比如说 `TreeNode`,`ListNode` 这种,在本地你还得把这个类 copy 过去。 | ||
|
||
而且在 IDE 上没办法测试,写完代码之后还得粘贴到网页上跑测试数据,那还不如直接网页上写呢。 | ||
|
||
算法又不是工程代码,量都比较小,IDE 的自动补全带来的收益基本可以忽略不计。 | ||
|
||
**2、实用** | ||
|
||
到时候面试的时候,面试官给你出的算法题大都是希望你直接在网页上完成的,最好是边写边讲你的思路。 | ||
|
||
如果平时练习的时候就习惯没有 IDE 的自动补全,习惯手写代码大脑编译,到时候面试的时候写代码就能更快更从容。 | ||
|
||
之前我面快手的时候,有个面试官让我 [实现 LRU 算法](https://labuladong.gitee.io/algo/),我直接把双链表的实现、哈希链表的实现,在网页上全写出来了,而且一次无 bug 跑通,可以看到面试官惊讶的表情😂 | ||
|
||
我秋招能当 offer 收割机,很大程度上就是因为手写算法这一关超出面试官的预期,其实都是因为之前在网页上刷题练出来的。 | ||
|
||
接下来分享我觉得最常实用的干货技巧。 | ||
|
||
### 如何给算法 debug | ||
|
||
代码的错误时无法避免的,有时候可能整个思路都错了,有时候可能是某些细节问题,比如 `i` 和 `j` 写反了,这种问题怎么排查? | ||
|
||
我想一般的算法问题肯定不难排查,肉眼检查应该都没啥问题,再不济 `print` 打印一些关键变量的值,总能发现问题。 | ||
|
||
**比较让人头疼的的应该是递归算法的问题排查**。 | ||
|
||
如果没有一定的经验,函数递归的过程很难被正确理解,所以这里就重点讲讲如何高效 debug 递归算法。 | ||
|
||
有的读者可能会说,把算法 copy 到 IDE 里面,然后打断点一步步跟着走不就行了吗? | ||
|
||
这个方法肯定是可以的,但是之前的文章多次说过,递归函数最好从一个全局的角度理解,而不要跳进具体的细节。 | ||
|
||
如果你对递归还不够熟悉,没有一个全局的视角,这种一步步打断点的方式也容易把人绕进去。 | ||
|
||
**我的建议是直接在递归函数内部打印关键值,配合缩进,直观地观察递归函数执行情况**。 | ||
|
||
最能提升我们 debug 效率的是缩进,除了解法函数,我们新定义一个函数 `printIndent` 和一个全局变量 `count`: | ||
|
||
```cpp | ||
// 全局变量,记录递归函数的递归层数 | ||
int count = 0; | ||
|
||
// 输入 n,打印 n 个 tab 缩进 | ||
void printIndent(int n) { | ||
for (int i = 0; i < n; i++) { | ||
printf(" "); | ||
} | ||
} | ||
``` | ||
接下来,套路来了: | ||
**在递归函数的开头,调用 `printIndent(count++)` 并打印关键变量;然后在所有 `return` 语句之前调用 `printIndent(--count)` 并打印返回值**。 | ||
举个具体的例子,比如说上篇文章 [练琴时悟出的一个动态规划算法](https://labuladong.gitee.io/algo/) 中实现了一个递归的 `dp` 函数,大致的结构如下: | ||
```cpp | ||
int dp(string& ring, int i, string& key, int j) { | ||
/* base case */ | ||
if (j == key.size()) { | ||
return 0; | ||
} | ||
/* 状态转移 */ | ||
int res = INT_MAX; | ||
for (int k : charToIndex[key[j]]) { | ||
res = min(res, dp(ring, j, key, i + 1)); | ||
} | ||
return res; | ||
} | ||
``` | ||
|
||
这个递归的 `dp` 函数在我进行了 debug 之后,变成了这样: | ||
|
||
```cpp | ||
int count = 0; | ||
void printIndent(int n) { | ||
for (int i = 0; i < n; i++) { | ||
printf(" "); | ||
} | ||
} | ||
|
||
int dp(string& ring, int i, string& key, int j) { | ||
// printIndent(count++); | ||
// printf("i = %d, j = %d\n", i, j); | ||
|
||
if (j == key.size()) { | ||
// printIndent(--count); | ||
// printf("return 0\n"); | ||
return 0; | ||
} | ||
|
||
int res = INT_MAX; | ||
for (int k : charToIndex[key[j]]) { | ||
res = min(res, dp(ring, j, key, i + 1)); | ||
} | ||
|
||
// printIndent(--count); | ||
// printf("return %d\n", res); | ||
return res; | ||
} | ||
``` | ||
**就是在函数开头和所有 `return` 语句对应的地方加上一些打印代码**。 | ||
如果去掉注释,执行一个测试用例,输出如下: | ||
![](../pictures/刷题技巧/1.jpg) | ||
这样,我们通过对比对应的缩进就能知道每次递归时输入的关键参数 `i, j` 的值,以及每次递归调用返回的结果是多少。 | ||
**最重要的是,这样可以比较直观地看出递归过程,你有没有发现这就是一棵递归树**? | ||
![](../pictures/刷题技巧/2.jpg) | ||
前文 [动态规划套路详解](https://labuladong.gitee.io/algo/) 说过,理解递归函数最重要的就是画出递归树,这样打印一下,连递归树都不用自己画了,而且还能清晰地看出每次递归的返回值。 | ||
**可以说,这是对刷题「幸福感」提升最大的一个小技巧,比 IDE 打断点要高效**。 | ||
好了,本文分享就到这里,马上快过年了,估计大家都无心学习了,不过刷题还是要坚持的,这就叫弯道超车,顺便实践一下这个技巧。 | ||
如果本文对你有帮助,点个在看,就会被推荐更多相似文章。 | ||
**_____________** | ||
**《labuladong 的算法小抄》已经出版,关注公众号「labuladong」查看详情;后台回复关键词「进群」可加入算法群,回复题号获取对应的文章**: | ||
![](../pictures/souyisou2.png) |
Oops, something went wrong.