0%

初识动态规划

动态规划可以非常显著的降低时间复杂度,提高代码的执行效率。

0-1背包问题

对于一组不同重量、不可分割的物品,选择一些装入背包,在满足背包最大重量限制的前提下,背包中物品总重量的最大值。

  1. 回溯算法
    穷举所有可能的装法,然后找出 满足条件的最大值。

    将回溯求解过程转换为递归树
    树中的每个节点表示一种状态,用(i,cw)表示。i表示将要决策第几个物品是否装入背包,cw表示当前背包中物品的总重量。
    在递归树中,有些子问题的求解是重复的,可以借助递归中的备忘录的解决方式,记录已经计算好的f(i,cw),当再次计算到重复的f(i,cw)时,直接从备忘录中取出来,避免冗余计算。

    回溯算法解决问题的时间复杂度是O(2n),是指数级的。

  2. 动态规划
    把整个求解过程分为多个阶段,每个阶段对应一个决策,记录每一阶段可达的状态集合(去掉重复的),通过当前阶段的状态集合,来推导下一阶段的状态集合,动态的往前推进。

    对应背包问题,把整个求解过程分为n个阶段,每个阶段会决策一个物品是否放到背包中。每个物品决策完之后,背包中的物品的重量会有多种情况,达到多种不同的状态,对应到递归树中,就是很多不同的节点。
    把每一层重复的状态(节点)合并,只记录不同的状态,然后基于上一层的状态集合,来推导下一层的状态集合。动态的往前推进。

    通过合并每一层重复的状态,这样保证每一层不同状态的个数都不会超过w个(如背包的承载重量w),成功避免了每层状态个数的指数级增长。

    最终,只需要在最后一层找一个值为true的最接近w的值,就是背包中物品总重量的最大值。

    动态规划代码中,耗时最多的部分就是代码中的两层for循环,时间复杂度是O(n*w)。n表示物品个数,w表示背包可以承载的总重量。
    代码中,需要额外申请一个n乘以w+1的二维数组,对空间的消耗比较多。动态规划是一种空间换时间的解决思路。

    一维数组优化,只记录当前层选择后的最终结果,节省空间,但是无法倒推放入了哪些物品。

    j从大到小处理,避免for循环重复计算的问题。

    比如 j = 0, item[i] = 5, w=10,如果正向循环,j=0 时会设置 state[5] = true, 而当遍历至 j=5时,由于 state[5]=true,会设置 state[10] = true,但是实际上将 5 这个重量使用了两次,所以导致了重量的重复使用。

0-1背包问题升级版

引入物品价值变量。
对于一组不同重量、不同价值、不可分割的物品,选择将某些物品装入背包,在满足背包最大重量限制的前提下,背包中可装入物品的总价值的最大值。

  1. 回溯算法

    递归树:
    在递归树中,每个节点表示一个状态。用三个变量(i,cw,cv)表示一个状态,其中i表示即将要决策第i个物品是否装入背包,cw表示当前背包中物品的总重量,cv表示当前背包中物品的总价值。
    在递归树中,有些节点的i和cw是完全相同的,在背包中物品总重量一样的情况下,某种状态对应的物品总价值更大,可以舍弃同种状态价值小的状态,沿着价值大的决策路线继续往下决策。
    对于(i,cw)相同的不同状态,只需要保留cv值最大的状态,继续递归处理,其他状态不予考虑。

  2. 动态规划
    把整个求解过程分为n个阶段,每个阶段决策一个物品是否放入背包中。每个阶段决策之后,背包中的物品的总重量以及总价值会有多种情况。
    用一个二维数组states[n][w+1],来记录每层可以达到的不同状态,数组中存储的是当前状态对应的最大总价值。把每一层中(i,cw)重复的状态节点合并,只记录cv值最大的那个状态,然后基于这些状态来推导下一层的状态。

    时间复杂度为O(n*w),空间复杂度O(n*w)。

结论

大部分动态规划能解决的问题,都可以通过回溯算法来解决。
回溯算法解决起来效率比较低,时间复杂度是指数级的。
动态规划在执行效率方面高很多,但是空间复杂度也提高了。
动态规划是一种空间换时间的算法思想。