|
35 | 35 | <img src='../pics/416.分割等和子集1.png' width=600> </img></div>
|
36 | 36 |
|
37 | 37 |
|
38 |
| -## 01 背包 |
39 |
| - |
40 |
| -有N件物品和一个最多能被重量为W 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。**每件物品只能用一次**,求解将哪些物品装入背包里物品价值总和最大。 |
41 |
| - |
42 |
| -这是标准的背包问题,以至于很多同学看了这个自然就会想到背包,甚至都不知道暴力的解法应该怎么解。 |
43 |
| - |
44 |
| -这样其实就是没有从底向上去思考,而是习惯性的只知道背包了,那么暴力的解法应该是怎么样的呢? |
45 |
| - |
46 |
| -每一件物品其实只有两个状态,取或者不取,所以可以使用回溯法搜索出所有的情况,那么时间复杂度就是O(2^n),这里的n表示物品数量。 |
47 |
| - |
48 |
| -所以暴力的解法是指数级别的时间复杂度。进而才需要动态规划的解法来进行优化! |
49 |
| - |
50 |
| -目前leetcode上没有发现有纯01背包的题目,leetcode上相关01背包问题都是需要某种条件转化为01背包问题,所以 我举一个纯01背包的例子来给大家讲解。 |
51 |
| - |
52 |
| -把01背包理论和代码理解透彻了,我们再刷leetcode上的题目。 |
53 |
| - |
54 |
| -下面的讲解中,我举一个例子: |
55 |
| - |
56 |
| -背包最大重量为4。 |
57 |
| - |
58 |
| -物品为: |
59 |
| - |
60 |
| -| | 重量 | 价值 | |
61 |
| -| --- | --- | --- | |
62 |
| -| 书 | 1 | 15 | |
63 |
| -| 台灯 | 3 | 20 | |
64 |
| -| 乐高 | 4 | 30 | |
65 |
| - |
66 |
| -以下讲解和图示中出现的数字都是以这个例子为例。 |
67 |
| - |
68 |
| -* 确定dp数组以及下标的含义 |
69 |
| - |
70 |
| -对于背包问题,有一种写法, 是使用二维数组,即**dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少**。 |
71 |
| - |
72 |
| -只看这个二维数组的定义,大家一定会有点懵,看下面这个图: |
73 |
| - |
74 |
| -<img src='../pics/动态规划-背包问题1.png' width=600> </img></div> |
75 |
| - |
76 |
| -要时刻记着这个dp数组的含义,下面的一些步骤都围绕这dp数组的含义进行的。 |
77 |
| - |
78 |
| -* dp数组如何初始化 |
79 |
| - |
80 |
| -**关于初始化,一定要和dp数组的定义吻合,否则到递推公式的时候就会越来越乱**。 |
81 |
| - |
82 |
| -首先从dp[i][j]的定义触发,如果背包容量j为0的话,无论是选取哪些物品,背包价值总和一定为0。如图: |
83 |
| - |
84 |
| -<img src='../pics/动态规划-背包问题2.png' width=600> </img></div> |
85 |
| - |
86 |
| - |
87 |
| -那么其他下标应该初始化多少呢? |
88 |
| - |
89 |
| -dp[i][j]在推导的时候一定是取价值最大的数,如果题目给的价值都是正整数那么非0下标都初始化为0就可以了,因为0就是最小的了,不会影响去最大价值的结果。 |
90 |
| - |
91 |
| -如果题目给的价值有负数,那么非0下标就要初始化为负无穷了。例如:一个物品的价值是-2,但对应的位置依然初始化为0,那么去最大值的时候,就会取0而不是-2了,所以要初始化为负无穷。 |
92 |
| - |
93 |
| -这样才能让dp数组在递归公式的过程中取的最大的价值,而不是被初始值覆盖了。 |
94 |
| - |
95 |
| -而本题价值都是正整数,所以初始化为0就可以了。 |
96 |
| - |
97 |
| -如图: |
98 |
| - |
99 |
| -<img src='../pics/动态规划-背包问题3.png' width=600> </img></div> |
100 |
| - |
101 |
| -**很明显,红框的位置就是我们要求的结果** |
102 |
| - |
103 |
| -* 确定递推公式 |
104 |
| - |
105 |
| -再回顾一下dp[i][j]的含义:从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。 |
106 |
| - |
107 |
| -那么可以有两个方向推出来dp[i][j], |
108 |
| - |
109 |
| -* 又dp[i - 1][j]推出,即不放背包里不放物品i的最大价值,此时dp[i][j]就是dp[i - 1][j] |
110 |
| -* 又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得到的最大价值 |
111 |
| - |
112 |
| -那么 dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); |
113 |
| - |
114 |
| -* 确定遍历顺序 |
115 |
| - |
116 |
| -确定递归公式之后,还要确定遍历顺序。 |
117 |
| - |
118 |
| -在如下图中,可以看出,有两个遍历的维度:物品与背包重量 |
119 |
| -<img src='../pics/动态规划-背包问题3.png' width=600> </img></div> |
120 |
| - |
121 |
| -那么问题来了,先遍历 物品还是先遍历背包重量呢? |
122 |
| - |
123 |
| -**其实都可以!! 但是先遍历物品更方便一些**。下面讲到具体原因的时候来在分析原因。 |
124 |
| - |
125 |
| -那么首先遍历物品,然后遍历背包重量。 |
126 |
| - |
127 |
| - |
128 |
| -************************ 首先我们来看dp数组的推导过程: |
129 |
| - |
130 |
| - |
131 |
| - |
132 |
| -注意 状态转移方程 dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); 中有两个下表为负数的情况,即:i - 1 和 j - weight[i]。 |
133 |
| - |
134 |
| -所以代码如下: |
135 |
| - |
136 |
| -``` |
137 |
| -for(int i = 1; i < weight.size(); i++) { // 遍历物品 |
138 |
| - for(int j = 0; j <= bagWeight; j++) { // 遍历背包重量 |
139 |
| - if (j < weight[i]) dp[i][j] = dp[i - 1][j]; |
140 |
| - else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); |
141 |
| -
|
142 |
| - } |
143 |
| -} |
144 |
| -``` |
145 |
| - |
146 |
| -来看一下对应的dp数组的数值,如图: |
147 |
| - |
148 |
| -<img src='../pics/动态规划-背包问题4.png' width=600> </img></div> |
149 |
| - |
150 |
| -建议大家此时自己在纸上推导一遍,看看dp数组里每一个数值是不是这样的。 |
151 |
| - |
152 |
| -**做动态规划的题目,最好的过程就是自己在纸上举一个例子把对应的dp数组的数值推导一下,然后在动手写代码!** |
153 |
| - |
154 |
| -很多同学做dp题目,遇到各种问题,然后凭感觉东改改西改改,怎么改都不对,或者稀里糊涂就改过了。 |
155 |
| - |
156 |
| -主要就是自己没有动手推导一下dp数组的演变过程,如果推导明白了,代码写出来就算有问题,只要把dp数组打印出来,对比一下和自己推导的有什么差异,很快就可以发现问题了。 |
157 |
| - |
158 |
| - |
159 |
| - |
160 |
| - |
161 |
| -因为 dp 每次用上一行的值进行计算的,没有重复利用本行的数值,所以不会重复使用同一个物品 |
162 |
| -``` |
163 |
| -for (int j = bagWeight; j >= weight[0]; j--) { |
164 |
| - dp[0][j] = dp[0][j - weight[0]] + value[0]; |
165 |
| -} |
166 |
| -
|
167 |
| -// weight数组的大小 就是物品个数 |
168 |
| -for(int i = 1; i < weight.size(); i++) { // 遍历物品 |
169 |
| - for(int j = 0; j <= bagWeight; j++) { // 遍历背包重量 |
170 |
| - if (j < weight[i]) dp[i][j] = dp[i - 1][j]; |
171 |
| - else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); |
172 |
| -
|
173 |
| - } |
174 |
| -} |
175 |
| -``` |
176 |
| - |
177 |
| -``` |
178 |
| -0 15 15 15 15 |
179 |
| -0 0 0 20 35 |
180 |
| -0 0 0 0 35 |
181 |
| -``` |
182 |
| - |
183 |
| - |
184 |
| -* 确定dp数组以及下标的含义 |
185 |
| - |
186 |
| -对于背包问题,有一种写法, 是使用二维数组,即dp[i][j] 表示从下标为0-i的物品里任意取,放进容量为j的背包,价值总和最大是多少。 |
187 |
| - |
188 |
| -对于这种写法,不仅空间多用了一个维度,而且思路比较绕。 |
189 |
| - |
190 |
| -我习惯直接使用一维数组(相对于二维数组,一维可以说是滚动数组)。在后面的讲解中,我也直接使用一维数组。 |
191 |
| - |
192 |
| -在一维dp数组中,dp[i]表示:容量为i的背包,所背的物品价值可以最大为dp[i] |
193 |
| - |
194 |
| - |
195 |
| -``` |
196 |
| -0 15 15 15 15 |
197 |
| -0 15 15 20 35 |
198 |
| -0 15 15 20 35 |
199 |
| -``` |
200 |
| - |
201 |
| -* dp数组如何初始化 |
202 |
| - |
203 |
| -**关于初始化,一定要和dp数组的定义吻合,否则到递推公式的时候就会越来越乱**。 |
204 |
| - |
205 |
| -dp[i]表示:容量为i的背包,所背的物品价值可以最大为dp[i],那么dp[0]就应该是0,因为背包容量为0所背的物品的最大价值就是0。 |
206 |
| - |
207 |
| -那么dp数组除了下标0的位置,初始为0,其他下标应该初始化多少呢? |
208 |
| - |
209 |
| -在回顾一下dp数组的含义:容量为i的背包,所背的物品价值可以最大为dp[i]。 |
210 |
| - |
211 |
| -那么dp数组在推导的时候一定是去价值最大的数,如果题目给的价值都是正整数那么非0下标都初始化为0就可以了,如果题目给的价值有负数,那么非0下标就要初始化为负无穷。 |
212 |
| - |
213 |
| -这样才能让dp数组在递归公式的过程中取的最大的价值,而不是被初始值覆盖了。 |
214 |
| - |
215 |
| -那么我假设物品价值都是大于0的,所以dp数组初始化的时候,都初始为0就可以了。 |
216 |
| - |
217 |
| -代码如下: |
218 |
| - |
219 |
| -``` |
220 |
| -vector<int> dp(背包容量V, 0); |
221 |
| -``` |
222 |
| - |
223 |
| -* 确定递推公式 |
224 |
| - |
225 |
| -有N件物品和一个容量为V 的背包。放入第i件物品耗费的空间是space[i],得到的价值是value[i] 。**每件物品只能用一次**,求解将哪些物品装入背包可使价值总和最大。 |
226 |
| - |
227 |
| -dp[j]为 容量为j的背包所背的最大价值,那么如何推导dp[j]呢? |
228 |
| - |
229 |
| -dp[j]可以通过dp[j - space[i]]推导出来,dp[j - space[i]]表示容量为j - space[i]的背包所背的最大价值。 |
230 |
| - |
231 |
| -dp[j - space[i]] + value[i] 表示 容量为 j - 物品i体积 的背包 加上 物品i的价值。 |
232 |
| - |
233 |
| -那么最大的dp[j]可能就是 dp[j - space[i]] + value[i]。 |
234 |
| - |
235 |
| -那么此时dp[j]有两个选择,一个是取子集dp[j],一个是取dp[j - space[i]] + value[i],指定是取最大的,毕竟是求最大价值, |
236 |
| - |
237 |
| -所以递归公式为: |
238 |
| - |
239 |
| -``` |
240 |
| -dp[j] = max(dp[j], dp[j - space[i]] + value[i]); |
241 |
| -``` |
242 |
| - |
243 |
| -* 确定遍历顺序 |
244 |
| - |
245 |
| -这里需要两层for循环,第一层遍历物品数量,第二层遍历背包的各个容量,来寻找最大值 |
246 |
| - |
247 |
| -****************** 讲讲for循环的顺序问题 |
248 |
| - |
249 |
| -代码如下: |
250 |
| - |
251 |
| -``` |
252 |
| -for(int i = 0; i < 物品数量N; i++) { // 遍历物品 |
253 |
| - for(int j = 背包容量V; j >= space[i]; j--) { // 遍历背包容量 |
254 |
| - dp[j] = max(dp[j], dp[j - space[i]] + value[i]); |
255 |
| - } |
256 |
| -} |
257 |
| -``` |
258 |
| - |
259 |
| -此时大家会发现为什么,第二层for循环要从大到小遍历。 |
260 |
| - |
261 |
| - |
262 |
| - |
263 |
| -注意这里第二个for循环是从大到小的,这样才能保证每件物品只使用一次。 |
264 |
| - |
265 |
| - |
266 |
| -如果物品装不满背包,dp[V]也是返回最大价值。 |
267 |
| - |
268 |
| - |
269 |
| -如果是第一种问法,要求恰好装满背包,那么在初始化时除了F [0]为0,其 它F [1..V ]均设为−∞,这样就可以保证最终得到的F [V ]是一种恰好装满背包的 最优解。 |
270 |
| -如果并没有要求必须把背包装满,而是只希望价格尽量大,初始化时应该 将F [0..V ]全部设为0。 |
271 |
| - |
272 |
| -这是为什么呢?可以这样理解:初始化的F 数组事实上就是在没有任何物 品可以放入背包时的合法状态。如果要求背包恰好装满,那么此时只有容量 为0的背包可以在什么也不装且价值为0的情况下被“恰好装满”,其它容量的 背包均没有合法的解,属于未定义的状态,应该被赋值为-∞了。如果背包并非 必须被装满,那么任何容量的背包都有一个合法解“什么都不装”,这个解的 价值为0,所以初始时状态的值也就全部为0了。 |
273 | 38 |
|
274 | 39 |
|
275 | 40 | # 完全背包
|
|
0 commit comments