1. 本系列计划
笔者大概预览了下《DSAA》的动态规划章节,不是很具体。经过一些思索及求教老司机,总结了下面的学习路线:
- 完成《背包9讲》
- 根据英文版《算法导论》详细学习DP章节
- 将Leetcode近期所保留的DP问题,重新做一遍。
本文适合那些和笔者一样,从来没有接触过动态规划的初学者。这样的学习计划,从某种程度是科学的。现在开始的引用都来自崔添翼的背包9讲。
2. 初识01背包
- 问题:有 件物品和一个容量为 的背包。放入第 i 件物品耗费的费用是 ,得到的价值是 。求解将哪些物品装入背包可使价值总和最大。
- 解法:使用 表示前 件物品恰放入一个容量为 的背包可以获得的最大价值。则其状态转移方程便是:
首先跳出来问题本身,思考为什么要定义子问题?虽然是初学者,但是观察不难发现,为了导出递推公式(又叫状态转移公式),那么以递推的方式,可以从初始条件得到最终问题的解。如果是求最优解,则将递推过程中的中间值进行比较(递推本身就要求存储上一次的值了),求得最终解。
上面的思考,是没有系统学习动态规划的人直观的感受,在以后的算法导论部分,会有更加准确的定义。回到这个问题,理解上面的解法不难,其他博文很多。
笔者的思考如下:
- 定义了状态(子问题): 代表当前 下的解,特别注意i代表前i个数。
-
到下一个状态的状态转换方程(特别与上文区分来真正理解转换方程的意义)
,不管怎么样都觉得别扭。也代表了在编程实现上更加复杂,不如上面的原式。(可以初步认为定义当前状态到上一个状态的递推更加适合编程实现)
理解两个状态的变迁,应抛开原来的问题,转化成思考两个状态的关系的问题,这往往不是困难的,所以DP问题的目前来看准确定义状态(子问题)是最核心步骤!
3. 优化01背包
F [0,0] F[0,1] ...F[0,V] 初始化为 0
for i=1 to N
for v= 0 to V
F [i, v]= max{F[i − 1, v],F[i−1,v−Ci]+Wi}
同构观察状态转移方程F[i,v]
与F[i-1,v]
和F[i-1,v-ci]
,将第二个for循环优化:
F [0,0] F[0,1] ...F[0,V] 初始化为 0
for i=1 to N
for v= Ci to V//v不用取到 0-Ci
F [i, v]= max{F[i − 1, v],F[i−1,v−Ci]+Wi}
思考算法本质,将F[x,y]
遍历,意味着最坏空间复杂度为
,时间复杂度为
。观察状态转移方程F[i,v]
只与F[i-1,v]
和F[i-1,v-ci]
有关,意味着可能在空间上的优化(只要F[i,v]
与部分密集的过去状态有关,都可能将空间压缩),此时将原本的矩阵压缩成一行,该方式笔者目前的理解是从二元状态转移方程优化得来,不清楚是不是能直接根据某种逻辑得到一维状态转移方程,以下为错误的伪代码:
F[0...V] 初始化为 0
for i=1 to N
for v=Ci to V
F [v] = max{F[v], F [v − Ci] + Wi}
上述伪代码在F[v-Ci]
时,可能因为旧值已经改变而发生错误。根据memcpy
中使用的解决内存重叠的技巧,这里也可以效仿,将v取V
到Ci
。得到如下:
F[0...V] 初始化为 0
for i=1 to N
for v=V to ci
F [v] = max{F[v], F [v − Ci] + Wi}
4. 初始化的细节问题
- 如果恰好装满背包,那么在初始化时除了F[0] 为 0,其它F[1…V] 均设为 ,这样就可以保证最终得到的 F[V]是一种恰好装满背包的最优解。
- 如果并没有要求必须把背包装满,而是只希望价格尽量大,初始化时应该将F[0…V]全部设为 0。
- 可以这样理解:初始化的 F 数组事实上就是在没有任何物品可以放入背包时的合法状态。如果要求背包恰好装满,那么此时只有容量为 0 的背包可以在什么也不装且价值为 0 的情况下被“恰好装满”,其它容量的背包均没有合法的解,属于未定义的状态,应该被赋值为 了。如果背包并非必须被装满,那么任何容量的背包都有一个合法解“什么都不装”,这个解的价值为 0,所以初始时状态的值也就全部为 0了。
以上原作者的论述,有一点不太充分:合理的解释了初值在不同前提条件的下的状态,但是状态转移方程是否还保持正确呢 ?
,假设前提是背包装满,那么
定义变成在前i个物品和背包容量
被装满的前提下的最大价值。此时状态转移方程依然符合!
换一个角度,如果是自己去做类似的题,从逻辑出发,一步步推导很可能写出来
的结论,因为我们总假设过去的状态是满的,只能通过扩充v的容量,来转移状态。但是在该前提下,第i个物件并没有被装入的情况依然是满足定义的,F[i-1,v]=F[i,v]=[i+1,v]....
都是符合前面状态定义的。
5. 总结
本文分析了一些在《背包九讲》中没有提及的细节,从初学者的角度最大程度的挖深思考空间,将今后学习动态规划的重点放在状态的定义上。上面的伪代码也可以用递归来实现,有的情况递归和迭代并不能相互代替,这在笔者以前的leetcode
总结中也提到过。