Knapsack背包问题 背包问题(Knapsack problem)是一种组合优化的NP完全问题。问题可以描述为:给定一组物品,每种物品都有自己的重量和价格,在限定的总重量内,我们如何选择,才能使得物品的总价格最高。问题的名称来源于如何选择最合适的物品放置于给定背包中。 也可以将背包问题描述为决定性问题,即在总重量不超过W的前提下,总价值是否能达到V。
0-1背包 最基础的背包问题:有N件物品和一个体积为V的背包, 每种物品均只有一件, 第i件物品的大小/重量是s[i],价值是v[i]. 求将哪些物品装入背包可使这些物品的体积总和不超过背包体积,且价值总和最大.
对于每一个物品,只有两种结果,放入或者不放入背包,那么kn(i, j)则表示背包容量剩余j时, 前i个物品能够达到的最大值:
kn1 = kn(i-1, j-s(i)) + v(i)表示物品i放入背包后的总价值, 为前i-1物品在第i个物品占用了背包容量s(i)后的的最优解加上第i个物品的价值v(i). kn2 = kn(i-1, j)表示物品i并没有放入背包, 等于前i-1个物品在相同背包容量的最优价值. 归纳出来的大小子问题间的关系(转移方程)为: kn(i, j) = max(kn1, kn2) = max(kn(i-1, j-s(i)) + v(i), kn(i-1, j)). 初始状态是对于不同背包剩余容量, 当没有物品可放时, 返回的最大价值一定是0. 所以背包问题, 就是二维的动态规划问题. 需要确定初始状态, 和哪些信息需要记忆.
可以简单地用一个二维数组记忆所有kn(i, j), 但要考虑到当容量非常大, 物品非常多时, 这个二维数组是很大的, 比如当(i, j) = (2000, 2000000), 会抛出java.lang.OutOfMemoryError: Java heap space. 特别是, 当每个物品的价值也比较大时, 二维数组的j维度其实利用率很低. 所以存在很多优化的空间.
优化的关键点在于减少记忆点. 注意到转移方程中:
kn(i, *)只需要用到kn(i-1, *)的值, 但我们又清楚地知道,物品在这里是没有顺序的意义的,所以这里的i仅仅是表示迭代的步骤, 只是为了遍历所有物品, 至于具体的顺序是不重要的, 所以不需要记录所有i对应的kn(i, *), 仅仅记录最近一次计算值即可. 所以我们只需要至多两个数组用来记录i-1和i对应的kn值. kn(i, j)要用到kn(i-1, k), k<=j的值, 具体要用到哪些k是取决于i. 所以j维度的值必须都要记录下来, 以防后续需要用到. 结合起来发现只需要一个一维数组kn = new int[size + 1]即可, i对应的值可以直接在数组上更新, 不需要额外的数组记录上一次迭代的值. 在实现中, 因为kn(i, j)要用到kn(i-1, <=j)的值, 也就是kn[<j]的值不能先于kn[j]更新, 所以kn的计算要从右往左(j = size; j--). 每次决定是否加入i物品之前, 如果剩余容量j小于s[i], 那么肯定无法放入, 这个判断可以融合进j的遍历中, 因为j本身代表了剩余容量. static int[] values; static int[] sizes; public static int knapsack(int size) { int n = values.length; int[] vs = new int[size + 1]; for (int i = 0; i < n; i++) { // items for (int j = size; j >= sizes[i]; j--) { vs[j] = Math.max(vs[j - sizes[i]] + values[i], vs[j]); } } return vs[size]; } 优化以后空间复杂度由$\theta(NS)$降到$\theta(S)$。但时间复杂度不变.
...