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-19 10:28
已编辑
成都理工大学 后端工程师
团孝子已上线feeling:面了很多家公司,能感受到目前只有小公司+外包喜欢问八股。大厂虽然也问八股,但是是从实习、项目中进行提问,并且大厂会问很深,面试官也会对你的回答进行思考➕追问,所以准备大厂面试前一定要备好相关资料。对于算法,我做的是codetop前100+力扣hot100+力扣高频150,面试中实感hot100就足够,基本上只要是hot100就秒答。对于项目和八股,我做的也是烂大街的星球项目,八股则是看小林和问ai,自己也写了很多技术博客和画了很多思维导图,并且自己也尝试用嘴巴说出来,不只停留于纸面。运气也很重要,必须要让面试官/HR看到简历才行,所以建议投递时间是下午两点。tl:第一岗位9.9&nbsp;投递9.10&nbsp;一面(一面评价:最近见过最强的大三,结束五分钟后约二面,都晚上九点了不下班吗)9.11&nbsp;二面(三道算法a出两道,反问评价:经验不够等横向,我实习生要啥经验)9.21挂(实习时间过短+其他原因,想要一年实习的,为什么不招个正职)第二岗位10.10投递10.11约面(主管打电话,说看到我之前投递记录了想要我挂qa职进去干后端,同意)10.14&nbsp;一面(无八股,主动说确实很强,意愿很强)10.16&nbsp;oc其余,友邦,东软,东华,惠择,用友oc已拒京东测开一面挂(投后端被测开捞)腾讯测试已拒(投后端被测开捞)ps:表扬惠择的主管面,没怎么问技术(可能是一面面试官沟通过了),全程一起讲大道理,解答了心中很多疑惑,也告诉我以面试官角度来看怎么选候选人,如果可以下次一定选惠择
HeaoDng:美团好像可以触发一面通
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务