背包问题.md 8.9 KB
Newer Older
L
labuladong 已提交
1 2
---
title: '动态规划之背包问题'
L
labuladong 已提交
3
tags: ['动态规划', '背包问题']
L
labuladong 已提交
4
---
L
labuladong 已提交
5 6 7

<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>
L
labuladong 已提交
8
<a href="https://appktavsiei5995.pc.xiaoe-tech.com/index" target="_blank"><img class="my_header_icon" src="https://img.shields.io/static/v1?label=精品课程&message=查看&color=pink&style=flat"></a>
L
labuladong 已提交
9 10 11 12
<a href="https://www.zhihu.com/people/labuladong"><img src="https://img.shields.io/badge/%E7%9F%A5%E4%B9%8E-@labuladong-000000.svg?style=flat-square&logo=Zhihu"></a>
<a href="https://space.bilibili.com/14089380"><img src="https://img.shields.io/badge/B站-@labuladong-000000.svg?style=flat-square&logo=Bilibili"></a>
</p>

L
labuladong 已提交
13
![](https://labuladong.github.io/pictures/souyisou1.png)
L
labuladong 已提交
14

L
labuladong 已提交
15
**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V2.1,[手把手刷二叉树系列课程](https://aep.xet.tech/s/3YGcq3) 上线。[第 18 期每日打卡](https://aep.xet.tech/s/2PLO1n) 开始报名。反馈或修正 chatGPT 翻译的多语言代码 [点击这里](https://github.com/labuladong/fucking-algorithm/issues/1113)。另外,建议你在我的 [网站](https://labuladong.github.io/algo/) 学习文章,体验更好。**
L
labuladong 已提交
16

L
labuladong 已提交
17 18 19 20


**-----------**

L
labuladong 已提交
21
> tip:本文有视频版:[0-1背包问题详解](https://www.bilibili.com/video/BV15B4y1P7X7/)。建议关注我的 B 站账号,我会用视频领读的方式带大家学习那些稍有难度的算法技巧。
L
labuladong 已提交
22 23 24 25 26 27 28

后台天天有人问背包问题,这个问题其实不难啊,如果我们号动态规划系列的十几篇文章你都看过,借助框架,遇到背包问题可以说是手到擒来好吧。无非就是状态 + 选择,也没啥特别之处嘛。

今天就来说一下背包问题吧,就讨论最常说的 0-1 背包问题。描述:

给你一个可装载重量为 `W` 的背包和 `N` 个物品,每个物品有重量和价值两个属性。其中第 `i` 个物品的重量为 `wt[i]`,价值为 `val[i]`,现在让你用这个背包装物品,最多能装的价值是多少?

L
labuladong 已提交
29
![](https://labuladong.github.io/pictures/knapsack/1.png)
L
labuladong 已提交
30

L
labuladong 已提交
31 32
举个简单的例子,输入如下:

L
labuladong 已提交
33
```py
L
labuladong 已提交
34 35 36 37 38 39 40 41 42
N = 3, W = 4
wt = [2, 1, 3]
val = [4, 2, 3]
```

算法返回 6,选择前两件物品装进背包,总重量 3 小于 `W`,可以获得最大价值 6。

题目就是这么简单,一个典型的动态规划问题。这个题目中的物品不可以分割,要么装进包里,要么不装,不能说切成两块装一半。这就是 0-1 背包这个名词的来历。

L
labuladong 已提交
43
解决这个问题没有什么排序之类巧妙的方法,只能穷举所有可能,根据我们 [动态规划详解](https://labuladong.github.io/article/fname.html?fname=动态规划详解进阶) 中的套路,直接走流程就行了。
L
labuladong 已提交
44 45 46

### 动规标准套路

L
labuladong 已提交
47
看来每篇动态规划文章都得重复一遍套路,历史文章中的动态规划问题都是按照下面的套路来的。
L
labuladong 已提交
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63

**第一步要明确两点,「状态」和「选择」**

先说状态,如何才能描述一个问题局面?只要给几个物品和一个背包的容量限制,就形成了一个背包问题呀。**所以状态有两个,就是「背包的容量」和「可选择的物品」**

再说选择,也很容易想到啊,对于每件物品,你能选择什么?**选择就是「装进背包」或者「不装进背包」嘛**

明白了状态和选择,动态规划问题基本上就解决了,只要往这个框架套就完事儿了:

```python
for 状态1 in 状态1的所有取值
    for 状态2 in 状态2的所有取值
        for ...
            dp[状态1][状态2][...] = 择优(选择1选择2...)
```

L
labuladong 已提交
64
> tip:此框架出自历史文章 [团灭 LeetCode 股票问题](https://labuladong.github.io/article/fname.html?fname=团灭股票问题)。
L
labuladong 已提交
65 66 67 68 69 70 71 72 73

**第二步要明确 `dp` 数组的定义**

首先看看刚才找到的「状态」,有两个,也就是说我们需要一个二维 `dp` 数组。

`dp[i][w]` 的定义如下:对于前 `i` 个物品,当前背包的容量为 `w`,这种情况下可以装的最大价值是 `dp[i][w]`

比如说,如果 `dp[3][5] = 6`,其含义为:对于给定的一系列物品中,若只对前 3 个物品进行选择,当背包容量为 5 时,最多可以装下的价值为 6。

L
labuladong 已提交
74
> info:为什么要这么定义?便于状态转移,或者说这就是套路,记下来就行了。建议看一下我们的动态规划系列文章,几种套路都被扒得清清楚楚了。
L
labuladong 已提交
75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101

根据这个定义,我们想求的最终答案就是 `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` 数组的定义:

L
labuladong 已提交
102
`dp[i][w]` 表示:对于前 `i` 个物品(从 1 开始计数),当前背包的容量为 `w` 时,这种情况下可以装下的最大价值是 `dp[i][w]`
L
labuladong 已提交
103 104 105

**如果你没有把这第 `i` 个物品装入背包**,那么很显然,最大价值 `dp[i][w]` 应该等于 `dp[i-1][w]`,继承之前的结果。

L
labuladong 已提交
106
**如果你把这第 `i` 个物品装入了背包**,那么 `dp[i][w]` 应该等于 `val[i-1] + dp[i-1][w - wt[i-1]]`
L
labuladong 已提交
107

L
labuladong 已提交
108
首先,由于数组索引从 0 开始,而我们定义中的 `i` 是从 1 开始计数的,所以 `val[i-1]``wt[i-1]` 表示第 `i` 个物品的价值和重量。
L
labuladong 已提交
109

L
labuladong 已提交
110
你如果选择将第 `i` 个物品装进背包,那么第 `i` 个物品的价值 `val[i-1]` 肯定就到手了,接下来你就要在剩余容量 `w - wt[i-1]` 的限制下,在前 `i - 1` 个物品中挑选,求最大价值,即 `dp[i-1][w - wt[i-1]]`
L
labuladong 已提交
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125

综上就是两种选择,我们都已经分析完毕,也就是写出来了状态转移方程,可以进一步细化代码:

```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]
```

**最后一步,把伪码翻译成代码,处理一些边界情况**

L
labuladong 已提交
126
我用 Java 写的代码,把上面的思路完全翻译了一遍,并且处理了 `w - wt[i-1]` 可能小于 0 导致数组索引越界的问题:
L
labuladong 已提交
127

L
labuladong 已提交
128
<!-- muliti_language -->
L
labuladong 已提交
129 130
```java
int knapsack(int W, int N, int[] wt, int[] val) {
L
labuladong 已提交
131
    assert N == wt.length;
L
labuladong 已提交
132
    // base case 已初始化
L
labuladong 已提交
133
    int[][] dp = new int[N + 1][W + 1];
L
labuladong 已提交
134 135
    for (int i = 1; i <= N; i++) {
        for (int w = 1; w <= W; w++) {
L
labuladong 已提交
136
            if (w - wt[i - 1] < 0) {
L
labuladong 已提交
137 138 139 140
                // 这种情况下只能选择不装入背包
                dp[i][w] = dp[i - 1][w];
            } else {
                // 装入或者不装入背包,择优
L
labuladong 已提交
141 142 143 144
                dp[i][w] = Math.max(
                    dp[i - 1][w - wt[i-1]] + val[i-1], 
                    dp[i - 1][w]
                );
L
labuladong 已提交
145 146 147 148 149 150 151 152
            }
        }
    }
    
    return dp[N][W];
}
```

L
labuladong 已提交
153
> note:其实函数签名中的物品数量 `N` 就是 `wt` 数组的长度,所以实际上这个参数 `N` 是多此一举的。但为了体现原汁原味的 0-1 背包问题,我就带上这个参数 `N` 了,你自己写的话可以省略。
L
labuladong 已提交
154

L
labuladong 已提交
155 156
至此,背包问题就解决了,相比而言,我觉得这是比较简单的动态规划问题,因为状态转移的推导比较自然,基本上你明确了 `dp` 数组的定义,就可以理所当然地确定状态转移了。

L
labuladong 已提交
157
接下来可阅读:
L
labuladong 已提交
158

L
labuladong 已提交
159 160
* [完全背包问题之零钱兑换](https://labuladong.github.io/article/fname.html?fname=背包零钱)
* [背包问题变体之子集分割](https://labuladong.github.io/article/fname.html?fname=背包子集)
L
labuladong 已提交
161 162 163



L
labuladong 已提交
164 165 166
<hr>
<details>
<summary><strong>引用本文的文章</strong></summary>
L
labuladong 已提交
167

L
labuladong 已提交
168 169 170 171 172 173 174 175 176 177 178 179 180
 - [扫描线技巧:安排会议室](https://labuladong.github.io/article/fname.html?fname=安排会议室)
 - [经典动态规划:子集背包问题](https://labuladong.github.io/article/fname.html?fname=背包子集)
 - [经典动态规划:完全背包问题](https://labuladong.github.io/article/fname.html?fname=背包零钱)
 - [经典回溯算法:集合划分问题](https://labuladong.github.io/article/fname.html?fname=集合划分)

</details><hr>





**_____________**

L
labuladong 已提交
181
**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「**进群**」可加入算法群;回复「**全家桶**」可下载配套 PDF 和刷题全家桶**
L
labuladong 已提交
182

L
labuladong 已提交
183
![](https://labuladong.github.io/pictures/souyisou2.png)