Banson's Blog
对01背包的一点体会

缘由

这是一个早就想解决的问题,曾经做01背包的练习就是单纯地套用模板,近期看到一篇很好的文章,又有了新的一些体悟,特此记录。

状态函数及状态转移方程:

函数 f(i,j) 表示考虑i 个物品,设定总空间j 时的最大价值。

w(i)>j 时, f(i,j)=f(i1,j) (放不下)

w(i)<=j 时,f(i,j)=max[f(i1,j),f(i1,jw(i))+v(i)]

误区纠正:学习方法

之前学习算法(尤其是DP),总是觉得自己应该整体上“理解”这个算法,而忽略了从列表、特殊数据等方面来辅助学习的方法。其实,如果对01背包问题感到难以理解,多半是因为不愿意认真列表分析一个特例。

状态函数

函数 f(i,j) 表示考虑i 个物品,设定总空间j 时的最大价值。

定义很清晰,但是理解时可能会不经意间进入下列误区:

  • i 的理解,应当是考察了前 i 个物品,并不是说这前 i 个物品都进入了背包;
  • j 的理解,是考察前 i 个物品时,赋予的总空间,而不是所谓“剩余空间”;
  • 所给空间 j 并不一定要恰好填满,而是不能超过。

状态转移方程

  • w(i)j 比较大小时,考察的是“就算把空间全都给第 i 件物品,它能不能放进去”,体现的是一种优先思想,即优先考虑第 i 件物品。先决定它放不放进去,然后再说编号 1(i1) 的物品如何处理。在这样的操作方法下,当第 i 件物品是否放入背包已经确定时, f(i,j) 最大的情况就等价于考察前 i 件物品最大时的情况。

时空复杂度的优化

该部分参考了dd_engi大佬的背包九讲

原问题较为显然的实现应当是定义二维数组 f[N][V] (其中 N 为物品数, V 为背包最大容量),然后以 i=0N 为外层循环, j=0V 为内层循环,“逐行填表”,最后输出 f[N][V] 即可。这样写的话时间和空间复杂度均为 O(NV) ,事实上,算法的复杂度还可以进一步地优化,其中空间复杂度优化最为显著。

先上一张图,便于下面进行描述。

空间复杂度的优化

注意到,在外层循环进行到某个特定的 i 时,进行内层循环时, i 相对于变化的 j来讲暂时不变, w[i] 自然也暂时不变。那么,若使 j 从后往前扫描(即 j=V,V1,,0 ),则每次可能用到的 f[i1][jw[i]] 的“正下方”那一格 f[i][jw[i]]暂时未被计算。

利用这一特性,不妨将数组减去一维,即将 f[N][V] 简化为 f[V] ,那么 f[i1][jw[i]] 就相应地改写为 f[jw[i]] 。由于 j 是从后向前遍历的,所以用到 f[jw[i]] 时,它的值还尚未被更新,实质上就相当于“上一行的” f[i1][jw[i]]

时间复杂度的优化

上面应该注意到,公式的

w(i)>j 时, f(i,j)=f(i1,j)

这一条并没有体现出来。

事实上,当 j<w[i] 时,由于 f(i,j)=f(i1,j)f[j] 就根本没有必要更新。于是,只需令 j=Vw[i] (而不是循环到 j=0 ),就可以使 j=0,1,,w[i]1 都不更新,相当于是“把上一行的值抄了下来”。

变形:分组背包

与传统01背包不同之处在于每次决策是决定是否从一组中拿一个物品,以及拿哪个(每一组中的物品最多只能拿一件)。

解决方法:略作变形,在决定 f[j] 的值的时候枚举一下正在考虑的这组中的所有物品,最终决定拿不拿/拿哪个。

总结

01背包可能是最经典、最简单的动态规划问题了。理解好这一模型的方法、思路、技巧,对后续动态规划的进一步学习应当有较大帮助。


Last modified on 2020-05-14