Skip to content

Commit e94b84c

Browse files
committed
update:更新核心算法套路
1 parent c967627 commit e94b84c

File tree

5 files changed

+646
-0
lines changed

5 files changed

+646
-0
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ GitHub Pages 地址:https://labuladong.github.io/algo/
7070
* [动态规划解题框架](动态规划系列/动态规划详解进阶.md)
7171
* [动态规划答疑篇](动态规划系列/最优子结构.md)
7272
* [回溯算法解题框架](算法思维系列/回溯算法详解修订版.md)
73+
* [提高刷题幸福感的小技巧](技术/刷题技巧.md)
7374
* [为了学会二分查找,我写了首诗](算法思维系列/二分查找详解.md)
7475
* [滑动窗口解题框架](算法思维系列/滑动窗口技巧.md)
7576
* [双指针技巧解题框架](算法思维系列/双指针技巧.md)
@@ -80,6 +81,7 @@ GitHub Pages 地址:https://labuladong.github.io/algo/
8081
* [动态规划答疑篇](动态规划系列/最优子结构.md)
8182
* [动态规划设计:最长递增子序列](动态规划系列/动态规划设计:最长递增子序列.md)
8283
* [编辑距离](动态规划系列/编辑距离.md)
84+
* [经典动态规划:0-1 背包问题](动态规划系列/背包问题.md)
8385
* [经典动态规划问题:高楼扔鸡蛋](动态规划系列/高楼扔鸡蛋问题.md)
8486
* [经典动态规划问题:高楼扔鸡蛋(进阶)](动态规划系列/高楼扔鸡蛋进阶.md)
8587
* [动态规划之子序列问题解题模板](动态规划系列/子序列问题模板.md)

pictures/souyisou.png

58.2 KB
Loading

动态规划系列/背包问题.md

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
# 动态规划之背包问题
2+
3+
4+
<p align='center'>
5+
<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>
6+
<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>
7+
<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>
8+
<a href="https://space.bilibili.com/14089380"><img src="https://img.shields.io/badge/B站[email protected]?style=flat-square&logo=Bilibili"></a>
9+
</p>
10+
11+
![](../pictures/souyisou.png)
12+
13+
相关推荐:
14+
* [经典动态规划:最长公共子序列](https://labuladong.gitee.io/algo/)
15+
* [特殊数据结构:单调栈](https://labuladong.gitee.io/algo/)
16+
17+
**-----------**
18+
19+
本文有视频版:[0-1背包问题详解](https://www.bilibili.com/video/BV15B4y1P7X7/)
20+
21+
后台天天有人问背包问题,这个问题其实不难啊,如果我们号动态规划系列的十几篇文章你都看过,借助框架,遇到背包问题可以说是手到擒来好吧。无非就是状态 + 选择,也没啥特别之处嘛。
22+
23+
今天就来说一下背包问题吧,就讨论最常说的 0-1 背包问题。描述:
24+
25+
给你一个可装载重量为 `W` 的背包和 `N` 个物品,每个物品有重量和价值两个属性。其中第 `i` 个物品的重量为 `wt[i]`,价值为 `val[i]`,现在让你用这个背包装物品,最多能装的价值是多少?
26+
27+
举个简单的例子,输入如下:
28+
29+
```
30+
N = 3, W = 4
31+
wt = [2, 1, 3]
32+
val = [4, 2, 3]
33+
```
34+
35+
算法返回 6,选择前两件物品装进背包,总重量 3 小于 `W`,可以获得最大价值 6。
36+
37+
题目就是这么简单,一个典型的动态规划问题。这个题目中的物品不可以分割,要么装进包里,要么不装,不能说切成两块装一半。这就是 0-1 背包这个名词的来历。
38+
39+
解决这个问题没有什么排序之类巧妙的方法,只能穷举所有可能,根据我们 [动态规划详解](https://labuladong.gitee.io/algo/) 中的套路,直接走流程就行了。
40+
41+
### 动规标准套路
42+
43+
看来我得每篇动态规划文章都得重复一遍套路,历史文章中的动态规划问题都是按照下面的套路来的。
44+
45+
**第一步要明确两点,「状态」和「选择」**
46+
47+
先说状态,如何才能描述一个问题局面?只要给几个物品和一个背包的容量限制,就形成了一个背包问题呀。**所以状态有两个,就是「背包的容量」和「可选择的物品」**
48+
49+
再说选择,也很容易想到啊,对于每件物品,你能选择什么?**选择就是「装进背包」或者「不装进背包」嘛**
50+
51+
明白了状态和选择,动态规划问题基本上就解决了,只要往这个框架套就完事儿了:
52+
53+
```python
54+
for 状态1 in 状态1的所有取值:
55+
for 状态2 in 状态2的所有取值:
56+
for ...
57+
dp[状态1][状态2][...] = 择优(选择1,选择2...)
58+
```
59+
60+
PS:此框架出自历史文章 [团灭 LeetCode 股票问题](https://labuladong.gitee.io/algo/)。
61+
62+
**第二步要明确 `dp` 数组的定义**
63+
64+
首先看看刚才找到的「状态」,有两个,也就是说我们需要一个二维 `dp` 数组。
65+
66+
`dp[i][w]` 的定义如下:对于前 `i` 个物品,当前背包的容量为 `w`,这种情况下可以装的最大价值是 `dp[i][w]`
67+
68+
比如说,如果 `dp[3][5] = 6`,其含义为:对于给定的一系列物品中,若只对前 3 个物品进行选择,当背包容量为 5 时,最多可以装下的价值为 6
69+
70+
PS:为什么要这么定义?便于状态转移,或者说这就是套路,记下来就行了。建议看一下我们的动态规划系列文章,几种套路都被扒得清清楚楚了。
71+
72+
根据这个定义,我们想求的最终答案就是 `dp[N][W]`。base case 就是 `dp[0][..] = dp[..][0] = 0`,因为没有物品或者背包没有空间的时候,能装的最大价值就是 0
73+
74+
细化上面的框架:
75+
76+
```python
77+
int[][] dp[N+1][W+1]
78+
dp[0][..] = 0
79+
dp[..][0] = 0
80+
81+
for i in [1..N]:
82+
for w in [1..W]:
83+
dp[i][w] = max(
84+
把物品 i 装进背包,
85+
不把物品 i 装进背包
86+
)
87+
return dp[N][W]
88+
```
89+
90+
**第三步,根据「选择」,思考状态转移的逻辑**
91+
92+
简单说就是,上面伪码中「把物品 `i` 装进背包」和「不把物品 `i` 装进背包」怎么用代码体现出来呢?
93+
94+
这就要结合对 `dp` 数组的定义,看看这两种选择会对状态产生什么影响:
95+
96+
先重申一下刚才我们的 `dp` 数组的定义:
97+
98+
`dp[i][w]` 表示:对于前 `i` 个物品,当前背包的容量为 `w` 时,这种情况下可以装下的最大价值是 `dp[i][w]`
99+
100+
**如果你没有把这第 `i` 个物品装入背包**,那么很显然,最大价值 `dp[i][w]` 应该等于 `dp[i-1][w]`,继承之前的结果。
101+
102+
**如果你把这第 `i` 个物品装入了背包**,那么 `dp[i][w]` 应该等于 `dp[i-1][w - wt[i-1]] + val[i-1]`
103+
104+
首先,由于 `i` 是从 1 开始的,所以 `val``wt` 的索引是 `i-1` 时表示第 `i` 个物品的价值和重量。
105+
106+
`dp[i-1][w - wt[i-1]]` 也很好理解:你如果装了第 `i` 个物品,就要寻求剩余重量 `w - wt[i-1]` 限制下的最大价值,加上第 `i` 个物品的价值 `val[i-1]`
107+
108+
综上就是两种选择,我们都已经分析完毕,也就是写出来了状态转移方程,可以进一步细化代码:
109+
110+
```python
111+
for i in [1..N]:
112+
for w in [1..W]:
113+
dp[i][w] = max(
114+
dp[i-1][w],
115+
dp[i-1][w - wt[i-1]] + val[i-1]
116+
)
117+
return dp[N][W]
118+
```
119+
120+
**最后一步,把伪码翻译成代码,处理一些边界情况**
121+
122+
我用 C++ 写的代码,把上面的思路完全翻译了一遍,并且处理了 `w - wt[i-1]` 可能小于 0 导致数组索引越界的问题:
123+
124+
```cpp
125+
int knapsack(int W, int N, vector<int>& wt, vector<int>& val) {
126+
// base case 已初始化
127+
vector<vector<int>> dp(N + 1, vector<int>(W + 1, 0));
128+
for (int i = 1; i <= N; i++) {
129+
for (int w = 1; w <= W; w++) {
130+
if (w - wt[i-1] < 0) {
131+
// 这种情况下只能选择不装入背包
132+
dp[i][w] = dp[i - 1][w];
133+
} else {
134+
// 装入或者不装入背包,择优
135+
dp[i][w] = max(dp[i - 1][w - wt[i-1]] + val[i-1],
136+
dp[i - 1][w]);
137+
}
138+
}
139+
}
140+
141+
return dp[N][W];
142+
}
143+
```
144+
145+
至此,背包问题就解决了,相比而言,我觉得这是比较简单的动态规划问题,因为状态转移的推导比较自然,基本上你明确了 `dp` 数组的定义,就可以理所当然地确定状态转移了。
146+
147+
接下来请阅读:
148+
149+
* [背包问题变体之子集分割](https://labuladong.gitee.io/algo/)
150+
* [完全背包问题之零钱兑换](https://labuladong.gitee.io/algo/)
151+
152+
**_____________**
153+
154+
**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitee.io/algo/) 持续更新最新文章**
155+
156+
**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**
157+
158+
<p align='center'>
159+
<img src="../pictures/qrcode.jpg" width=200 >
160+
</p>

技术/刷题技巧.md

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
# 刷题小技巧
2+
3+
4+
<p align='center'>
5+
<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>
6+
<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>
7+
<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>
8+
<a href="https://space.bilibili.com/14089380"><img src="https://img.shields.io/badge/B站[email protected]?style=flat-square&logo=Bilibili"></a>
9+
</p>
10+
11+
![](../pictures/souyisou.png)
12+
13+
相关推荐:
14+
* [一文解决三道区间问题](https://labuladong.gitee.io/algo/)
15+
* [Union-Find算法详解](https://labuladong.gitee.io/algo/)
16+
17+
**-----------**
18+
19+
相信每个人都有过被代码的小 bug 搞得心态爆炸的经历,本文分享一个我最常用的简单技巧,可以大幅提升刷题的幸福感。
20+
21+
在这之前,首先回答一个问题,刷力扣题是直接在网页上刷比较好还是在本地 IDE 上刷比较好?
22+
23+
如果是牛客网笔试那种自己处理输入输出的判题形式,一定要在 IDE 上写,这个没啥说的,但**像力扣这种判题形式,我个人偏好直接在网页上刷**,原因有二:
24+
25+
**1、方便**
26+
27+
因为力扣有的数据结构是自定的,比如说 `TreeNode``ListNode` 这种,在本地你还得把这个类 copy 过去。
28+
29+
而且在 IDE 上没办法测试,写完代码之后还得粘贴到网页上跑测试数据,那还不如直接网页上写呢。
30+
31+
算法又不是工程代码,量都比较小,IDE 的自动补全带来的收益基本可以忽略不计。
32+
33+
**2、实用**
34+
35+
到时候面试的时候,面试官给你出的算法题大都是希望你直接在网页上完成的,最好是边写边讲你的思路。
36+
37+
如果平时练习的时候就习惯没有 IDE 的自动补全,习惯手写代码大脑编译,到时候面试的时候写代码就能更快更从容。
38+
39+
之前我面快手的时候,有个面试官让我 [实现 LRU 算法](https://labuladong.gitee.io/algo/),我直接把双链表的实现、哈希链表的实现,在网页上全写出来了,而且一次无 bug 跑通,可以看到面试官惊讶的表情😂
40+
41+
我秋招能当 offer 收割机,很大程度上就是因为手写算法这一关超出面试官的预期,其实都是因为之前在网页上刷题练出来的。
42+
43+
接下来分享我觉得最常实用的干货技巧。
44+
45+
### 如何给算法 debug
46+
47+
代码的错误时无法避免的,有时候可能整个思路都错了,有时候可能是某些细节问题,比如 `i``j` 写反了,这种问题怎么排查?
48+
49+
我想一般的算法问题肯定不难排查,肉眼检查应该都没啥问题,再不济 `print` 打印一些关键变量的值,总能发现问题。
50+
51+
**比较让人头疼的的应该是递归算法的问题排查**
52+
53+
如果没有一定的经验,函数递归的过程很难被正确理解,所以这里就重点讲讲如何高效 debug 递归算法。
54+
55+
有的读者可能会说,把算法 copy 到 IDE 里面,然后打断点一步步跟着走不就行了吗?
56+
57+
这个方法肯定是可以的,但是之前的文章多次说过,递归函数最好从一个全局的角度理解,而不要跳进具体的细节。
58+
59+
如果你对递归还不够熟悉,没有一个全局的视角,这种一步步打断点的方式也容易把人绕进去。
60+
61+
**我的建议是直接在递归函数内部打印关键值,配合缩进,直观地观察递归函数执行情况**
62+
63+
最能提升我们 debug 效率的是缩进,除了解法函数,我们新定义一个函数 `printIndent` 和一个全局变量 `count`
64+
65+
```cpp
66+
// 全局变量,记录递归函数的递归层数
67+
int count = 0;
68+
69+
// 输入 n,打印 n 个 tab 缩进
70+
void printIndent(int n) {
71+
for (int i = 0; i < n; i++) {
72+
printf(" ");
73+
}
74+
}
75+
```
76+
77+
接下来,套路来了:
78+
79+
**在递归函数的开头,调用 `printIndent(count++)` 并打印关键变量;然后在所有 `return` 语句之前调用 `printIndent(--count)` 并打印返回值**。
80+
81+
举个具体的例子,比如说上篇文章 [练琴时悟出的一个动态规划算法](https://labuladong.gitee.io/algo/) 中实现了一个递归的 `dp` 函数,大致的结构如下:
82+
83+
```cpp
84+
int dp(string& ring, int i, string& key, int j) {
85+
/* base case */
86+
if (j == key.size()) {
87+
return 0;
88+
}
89+
90+
/* 状态转移 */
91+
int res = INT_MAX;
92+
for (int k : charToIndex[key[j]]) {
93+
res = min(res, dp(ring, j, key, i + 1));
94+
}
95+
96+
return res;
97+
}
98+
```
99+
100+
这个递归的 `dp` 函数在我进行了 debug 之后,变成了这样:
101+
102+
```cpp
103+
int count = 0;
104+
void printIndent(int n) {
105+
for (int i = 0; i < n; i++) {
106+
printf(" ");
107+
}
108+
}
109+
110+
int dp(string& ring, int i, string& key, int j) {
111+
// printIndent(count++);
112+
// printf("i = %d, j = %d\n", i, j);
113+
114+
if (j == key.size()) {
115+
// printIndent(--count);
116+
// printf("return 0\n");
117+
return 0;
118+
}
119+
120+
int res = INT_MAX;
121+
for (int k : charToIndex[key[j]]) {
122+
res = min(res, dp(ring, j, key, i + 1));
123+
}
124+
125+
// printIndent(--count);
126+
// printf("return %d\n", res);
127+
return res;
128+
}
129+
```
130+
131+
**就是在函数开头和所有 `return` 语句对应的地方加上一些打印代码**。
132+
133+
如果去掉注释,执行一个测试用例,输出如下:
134+
135+
![](../pictures/刷题技巧/1.jpg)
136+
137+
这样,我们通过对比对应的缩进就能知道每次递归时输入的关键参数 `i, j` 的值,以及每次递归调用返回的结果是多少。
138+
139+
**最重要的是,这样可以比较直观地看出递归过程,你有没有发现这就是一棵递归树**?
140+
141+
![](../pictures/刷题技巧/2.jpg)
142+
143+
前文 [动态规划套路详解](https://labuladong.gitee.io/algo/) 说过,理解递归函数最重要的就是画出递归树,这样打印一下,连递归树都不用自己画了,而且还能清晰地看出每次递归的返回值。
144+
145+
**可以说,这是对刷题「幸福感」提升最大的一个小技巧,比 IDE 打断点要高效**。
146+
147+
好了,本文分享就到这里,马上快过年了,估计大家都无心学习了,不过刷题还是要坚持的,这就叫弯道超车,顺便实践一下这个技巧。
148+
149+
如果本文对你有帮助,点个在看,就会被推荐更多相似文章。
150+
151+
**_____________**
152+
153+
**《labuladong 的算法小抄》已经出版,关注公众号「labuladong」查看详情;后台回复关键词「进群」可加入算法群,回复题号获取对应的文章**:
154+
155+
![](../pictures/souyisou2.png)

0 commit comments

Comments
 (0)