01背包与完全背包问题

1.递归解法

public static int knapsack(int capacity, int[] weights, int[] values) {
        int n = weights.length;
        //递归的套路,加一个index索引,控制递归到了哪一层
        return bestValue(capacity,n-1,weights,values);
    }

    private static int bestValue(int capacity, int index, int[] weights, int[] values){
        //递归的第一步:终止条件,没有商品了或者背包没容量了就终止
        if(index <= 0 || capacity<= 0){
            return 0;
        }
        //不放的情况
        int res = bestValue(capacity,index-1,weights,values);
        //放的情况,需要剩余满足容量>=weights[index]
        if(capacity >= weights[index]){
            //放的情况为bestValue(capacity - weights[index],index-1,weights,values)。也就是index放进去。其他的容量放
            //前面index-1
            res = Math.max(res,values[index]+bestValue(capacity - weights[index],index-1,weights,values));
        }
        return res;
    }

递归解法存在大量的重复计算。导致效率不高。下面可以进一步优化。

--------------------------------------------------------------------------------------------

2.记忆搜索解法。

申请一个二维数组用来记录重叠子问题。用空间换时间。

public static int knapsack(int capacity, int[] weights, int[] values) {
        int n = weights.length;
        //因为有商品和价值两个维度,所以需要用一个二维的空间来记录
        memo = new int[n][capacity+1];
        for (int i = 0; i < n; i++) {
            for (int j = 0; j <= capacity ; j++) {
                memo[i][j] = -1;

            }
        }
        return bestValue(capacity,n-1,weights,values);
    }

    private static int bestValue(int capacity, int index, int[] weights, int[] values){
        //递归的第一步:终止条件,没有商品了或者背包没容量了就终止
        if(index <= 0 || capacity<= 0){
            return 0;
        }
        //如果已经有缓存,直接从缓存中取即可,避免重复计算
        if (memo[index][capacity] != -1){
            return memo[index][capacity];
        }
        //不放的情况
        int res = bestValue(capacity,index-1,weights,values);
        //放的情况,需要剩余满足容量>=weights[index]
        if(capacity >= weights[index]){
            //放的情况为bestValue(capacity - weights[index],index-1,weights,values)。也就是index放进去。其他的容量放
            //前面index-1
            res = Math.max(res,values[index]+bestValue(capacity - weights[index],index-1,weights,values));
        }
        //将计算的结果加入缓存
        memo[index][capacity] = res;
        return res;
    }

记忆搜索虽然在时间复杂度上已经满足要求,但是递归需要大量堆栈上的空间,容易造成栈溢出。最好能将递归转换成递推。

==========================================================================

3.动态规划解法。动态规划其实就是一个填表的过程。下面举个例子说明整个过程。

假设有一个容量为5的背包以及3个物品

申请一个二维表格dp,并将第一行和第一列初始化。初始化的规则为。

对于第一行,第一行表示只有一个物品。所以只要capacity >= weights[0],就初始化为values[0]。

对于地理列,第一列表示背包容量为0的时候,所以第一列的值都为0

然后填dp[1][1]

public static int knapsack(int capacity, int[] weights, int[] values) {
        assert (weights.length == weights.length);
        int len = weights.length;
        if(len == 0){
            return 0;
        }
        //dp表示i个物品放进容量为c的背包的效益
        int[][] dp = new int[len][capacity+1];
        //初始化第一列,第一列表示背包容量为0的时候,所以第一列的值都为0
        for (int i = 0; i < len ; i++) {
            dp[i][0] = 0;
        }
        //初始化第一行。第一行表示只有一个物品。所以只要capacity >= weights[0],就初始化为values[0]
        for (int i = 1; i <= capacity ; i++) {
            dp[0][i] = capacity >= weights[0]?values[0]:0;
        }
        for (int i = 1; i < len ; i++) {
            for (int j = 1; j <= capacity ; j++) {
                //第i个物品放不时的结果,和只有i-1个物品的结果一样
                dp[i][j] = dp[i-1][j];
                //第i个物品能放下
                if(j >= weights[i]){
                    //比较放和不放哪种能产生更高的价值
                    dp[i][j] = Math.max(dp[i][j],values[i]+dp[i-1][j-weights[i]]);
                }
            }
        }
        return dp[len-1][capacity];

    }

=======================================================================

4.空间压缩。从填表的过程可以看出。其实每次只依赖上一行。因此用一个2行的数组即可表示整个过程。

public static int knapsackDp(int capacity, int[] weights, int[] values) {
    assert (weights.length == weights.length);
    int len = weights.length;
    if(len == 0){
        return 0;
    }
    //dp表示i个物品放进容量为c的背包的效益
    int[][] dp = new int[2][capacity+1];
    for (int i = 0; i < 2 ; i++) {
        dp[i][0] = 0;
    }
    for (int i = 1; i <= capacity ; i++) {
        dp[0][i] = capacity >= weights[0]?values[0]:0;
    }
    for (int i = 1; i < len ; i++) {
        for (int j = 1; j <= capacity ; j++) {
            dp[i%2][j] = dp[(i-1)%2][j];
            //第i个物品能放下
            if(j >= weights[i]){
                dp[i%2][j] = Math.max(dp[(i-1)%2][j],values[i]+dp[(i-1)%2][j-weights[i]]);

            }
        }
    }
    return dp[(len-1)%2][capacity];
}

=================================================================

5.空间继续压缩。用一行也可以。

public static int knapsack(int capacity, int[] weights, int[] values) {
        assert (weights.length == weights.length);
        int len = weights.length;
        if(len == 0){
            return 0;
        }
        //dp表示i个物品放进容量为capacity的背包的效益
        int[] dp = new int[capacity+1];
        //只放第0个物品,只有能放下。产生的价值就是values[0],否则为0
        for (int i = 0; i <= capacity ; i++) {
            dp[i] = i >= weights[0]?values[0]:0;
        }
        for (int i = 1; i < len ; i++) {
            //注意一定为倒序,j < weights[i]的话,表示放不下了。那么可以终止循环。这是动态规划中
            //一个常用的做法。减枝
            for (int j = capacity; j >= weights[i]; j--) {
                dp[j] = Math.max(dp[j],values[i]+dp[j-weights[i]]);

            }
        }
        return dp[capacity];

    }

为什么要强调递推过程是逆序的呢?

举个例子

假设n=1,背包容量c=5,只有一个物品,w和v都为1,倒着推得结果为
      0     1     2     3     4     5
1     0     1     1     1     1     1

正着推得结果为
      0     1     2     3     4     5
1     0     1     2     3     4     5

其实正向递推为完全背包。

因为倒着推。每个物品只有一次选择的机会,放或者不放。

正向推。每个物品在每次都可以选择放或者不放。所以是完全背包。
完全背包代码如下

public static int knapsack(int capacity, int[] weights, int[] values) {
        assert (weights.length == weights.length);
        int len = weights.length;
        if (len == 0) {
            return 0;
        }
        //dp表示i个物品放进容量为capacity的背包的效益
        int[] dp = new int[capacity + 1];
        for (int i = 0; i < len; i++) {
            //必须满足容量>=weights[i]才能放入
            for (int j = weights[i]; j <= capacity; j++) {
                dp[j] = Math.max(dp[j], values[i] + dp[j - weights[i]]);
            }
        }
        return dp[capacity];

    }

一些背包问题相关的算法题解析请参考此博文

全部评论

相关推荐

10-07 20:48
门头沟学院 Java
听说改名就会有offer:可能是实习上着班想到后面还要回学校给导师做牛马,看着身边都是21-25的年纪,突然emo了了
点赞 评论 收藏
分享
比亚迪汽车新技术研究院 硬件工程师 总包21左右 硕士
点赞 评论 收藏
分享
点赞 收藏 评论
分享
牛客网
牛客企业服务