概述

背包问题(Knapsack problem)是一种组合优化的NP完全问题。问题可以描述为:给定一组物品,每种物品都有自己的重量和价格,在限定的总重量内,我们如何选择,才能使得物品的总价格最高。问题的名称来源于如何选择最合适的物品放置于给定背包中。

定义

我们有 n 种物品,物品 j 的重量为wj,价格为pj。 我们假定所有物品的重量和价格都是非负的。背包所能承受的最大重量为W

如果限定每种物品只能选择0个或1个,则问题称为0-1背包问题

如果限定物品j最多只能选择bj个,则问题称为有界背包问题

如果不限定每种物品的数量,则问题称为无界背包问题

动态规划

动态规划背后的基本思想非常简单。大致上,若要解一个给定问题,我们需要解其不同部分(即子问题),再根据子问题的解以得出原问题的解。

通常许多子问题非常相似,为此动态规划法试图仅仅解决每个子问题一次,从而减少计算量:一旦某个给定子问题的解已经算出,则将其记忆化存储,以便下次需要同一个子问题解之时直接查表。

思路

举例,令物品数量N=5,背包所能承受的最大重量为W=10,物品与价格的对应关系如下表左三列所示。

name weight value 1 2 3 4 5 6 7 8 9 10
a 2 6 0 6 6 6 6 6 6 6 6 6
b 2 3 0 6 6 9 9 9 9 9 9 9
c 6 5 0 6 6 9 9 9 9 11 11 14
d 5 4 0 6 6 9 9 9 10 11 13 14
e 4 6 0 6 6 9 9 12 12 15 15 15

当放入物品a时,在背包所能承受的重量内,计算背包拥有的物品总价格,并进行标记,如表格第一行所示,当背包所能承受的重量大于等于2时,都可以放入物品a,背包拥有的物品总价格为6。

接着我们放入物品b,放入之前,一是要判断背包是否所能承受其重量,二是判断放入之后与放入之前拥有的物品总价格哪个最大,如表格第二行所示,当背包所能承受的重量大于等于2时,都可以放入物品b,但是,物品b在背包容量为[2,3]的时候,放入之后的总价格3不如放入之前的总价格6大,所以不放入。

当背包所能承受的重量等于4时,放入物品b后,背包所能承受的重量4减去物品b的重量2后,剩余的所能承受的重量2还可以放入物品a,此时背包拥有的物品总价格为物品a和物品b的总价格之和,即为9,大于放入之前的物品总价格6,所以此时背包拥有的物品总价格最大为9。

分析可知,在一层循环遍历下,我们需要一个一维数组保存背包所能承受的最大重量与其拥有的物品总价格,并不断更新。

代码

/**
 * 0-1背包问题(二维数组)
 *
 * @param N      物品数量
 * @param W      背包容量
 * @param weight 物品重量
 * @param value  物品价格
 * @return 最大价值
 */
public static int traceBack(int N, int W, int[] weight, int[] value) {
    // N 个物品 N行,0 行需要空出来,N+1;W 是容量的最大索引,长度为 W+1
    int[][] dp = new int[N + 1][W + 1];
    // 循环放入物品
    for (int i = 0; i < N; i++) {
        // 循环增大背包容量
        for (int j = 0; j <= W; j++) {
            // 只要背包容量大于等于物品重量就尝试放入
            if (j >= weight[i]) {
                // 比较放入物品之前与放入之后的价值哪个大
                dp[i + 1][j] = Math.max(dp[i][j], dp[i][j - weight[i]] + value[i]);
            }
        }
    }
    return dp[N][W];
}

/**
 * 0-1背包问题(一维数组)
 *
 * @param N      物品数量
 * @param W      背包容量
 * @param weight 物品重量
 * @param value  物品价格
 * @return 最大价值
 */
public static int traceBack(int N, int W, int[] weight, int[] value) {
    int[] dp = new int[W + 1]; // 范围[0,W] 当前背包容量对应的物品总价值
    for (int i = 0; i < N; i++) { // 一件、一件的向包中放入每件物品
        for (int j = W; j >= weight[i]; j--) { //只要背包可以放入就放
            dp[j] = Math.max(dp[j - weight[i]] + value[i], dp[j]); // 比较放入物品之前与放入之后的价值哪个大
        }
    }
    return dp[W];
}

复杂度

显而易见,算法需要的时间复杂度为O(NW),空间的消耗 即所需的一维数组为O(W)、二维数组为O(NW)。