《DP学习系列》从零开始学习动态规划,初识01背包(一)

1. 本系列计划

  笔者大概预览了下《DSAA》的动态规划章节,不是很具体。经过一些思索及求教老司机,总结了下面的学习路线:
  

  • 完成《背包9讲》
  • 根据英文版《算法导论》详细学习DP章节
  • 将Leetcode近期所保留的DP问题,重新做一遍。

  本文适合那些和笔者一样,从来没有接触过动态规划的初学者。这样的学习计划,从某种程度是科学的。现在开始的引用都来自崔添翼的背包9讲

2. 初识01背包

  • 问题:有 N 件物品和一个容量为 V 的背包。放入第 i 件物品耗费的费用是 C i ,得到的价值是 W i 。求解将哪些物品装入背包可使价值总和最大。
  • 解法:使用 F [ i , v ] 表示前 i 件物品恰放入一个容量为 v 的背包可以获得的最大价值。则其状态转移方程便是:
       F [ i , v ] = m a x { F [ i 1 , v ] , F [ i 1 , v C i ] + W i }

  首先跳出来问题本身,思考为什么要定义子问题?虽然是初学者,但是观察不难发现,为了导出递推公式(又叫状态转移公式),那么以递推的方式,可以从初始条件得到最终问题的解。如果是求最优解,则将递推过程中的中间值进行比较(递推本身就要求存储上一次的值了),求得最终解。
  上面的思考,是没有系统学习动态规划的人直观的感受,在以后的算法导论部分,会有更加准确的定义。回到这个问题,理解上面的解法不难,其他博文很多。
  笔者的思考如下:

  • 定义了状态(子问题): F [ i , v ] 代表当前 i , v 下的解,特别注意i代表前i个数
  • F [ i , v ] 到下一个状态的状态转换方程(特别与上文区分来真正理解转换方程的意义
    F [ i + 1 , k ] = m a x { F [ i , v + C i + 1 ] + W i + 1 , F [ i , v ] } ,不管怎么样都觉得别扭。也代表了在编程实现上更加复杂,不如上面的原式。(可以初步认为定义当前状态到上一个状态的递推更加适合编程实现


  理解两个状态的变迁,应抛开原来的问题,转化成思考两个状态的关系的问题,这往往不是困难的,所以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]遍历,意味着最坏空间复杂度为 O ( N V ) ,时间复杂度为 O ( N V ) 。观察状态转移方程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取VCi。得到如下:

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了。

  以上原作者的论述,有一点不太充分:合理的解释了初值在不同前提条件的下的状态,但是状态转移方程是否还保持正确呢 ?
   F [ i , v ] = m a x { F [ i 1 , v ] , F [ i 1 , v C i ] + W i } ,假设前提是背包装满,那么 F [ i , v ] 定义变成在前i个物品和背包容量 v 被装满的前提下的最大价值。此时状态转移方程依然符合!
  换一个角度,如果是自己去做类似的题,从逻辑出发,一步步推导很可能写出来 F [ i , v ] = F [ i 1 , v C i ] + W i 的结论,因为我们总假设过去的状态是满的,只能通过扩充v的容量,来转移状态。但是在该前提下,第i个物件并没有被装入的情况依然是满足定义的,F[i-1,v]=F[i,v]=[i+1,v]....都是符合前面状态定义的。

5. 总结

  本文分析了一些在《背包九讲》中没有提及的细节,从初学者的角度最大程度的挖深思考空间,将今后学习动态规划的重点放在状态的定义上。上面的伪代码也可以用递归来实现,有的情况递归和迭代并不能相互代替,这在笔者以前的leetcode总结中也提到过。  

猜你喜欢

转载自blog.csdn.net/lovestackover/article/details/80543118