|
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