代码随想录算法训练营第44天 | ● 完全背包● 518. 零钱兑换 II ● 377. 组合总和 Ⅳ

文章目录

  • 前言
  • 一、完全背包
  • 二、518. 零钱兑换 II
  • 三、377. 组合总和 Ⅳss
  • 四、爬楼梯(进阶)
  • 总结

前言

完全背包;


一、完全背包

前情回顾,01背包中二维dp数组的两个for遍历的先后循序是可以颠倒了,一维dp数组的两个for循环先后循序一定是先遍历物品,再遍历背包容量。

对于01背包的一维数组:

首先,其背包的遍历顺序是倒序,因为01背包只允许取一个,倘若是正序,对于dp[j] = dp[j-weigh[i]) + value[i] ,会重复取多次,不符合01背包的定义;因而01背包的两层for循环,先循环物品,后循环背包,因为这样只会取一次(横向遍历);而先循环背包,后循环物品(纵向遍历),那么至多只能放一种,举个例子

公式:dp[j] = dp[j-weigh[i]) + value[i]  + 背包是倒序

1.先物品后背包: (横向)在i = 1,也就是第二行时,dp[4] = dp[4-3]+20 = dp[1]+20,在i=0时,dp[1] = 15;因而dp[4] = 15+20 = 35;

2.先背包后物品:(纵向)因为是倒序遍历背包,在最后一列,当i=1,第二行时,dp[4] = dp[4-3]+20 = dp[1]+20,但因为是纵向,dp[1]还没有被赋值,仍是0,所以dp[4] = 0+20=20;

完全背包和01背包问题唯一不同的地方就是,每种物品有无限件对于纯完全背包问题,其for循环的先后循环是可以颠倒的!

因为dp[j] 是根据 下标j之前所对应的dp[j]计算出来的。 只要保证下标j之前的dp[j]都是经过计算的就可以了。

代码:(先物品)

    for (int i = 0; i < weight.length; i++){ // 遍历物品
        for (int j = weight[i]; j <= bagWeight; j++){ // 遍历背包容量
            dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
        }
    }

代码:(先背包,注意这里已经是正序,且代码相对逆序有所变动)

    for (int j = 1; j <= bagWeight; j++){ // 遍历背包容量
        for (int i = 0; i < weight.length; i++){ // 遍历物品
            if (j - weight[i] >= 0){
                dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
            }
        }
    }

二、518. 零钱兑换 II

递推公式:

dp[j],j 为5,

  • 已经有一个1(nums[i]) 的话,有 dp[4]种方法 凑成 容量为5的背包。
  • 已经有一个2(nums[i]) 的话,有 dp[3]种方法 凑成 容量为5的背包。
  • 已经有一个3(nums[i]) 的话,有 dp[2]中方法 凑成 容量为5的背包
  • 已经有一个4(nums[i]) 的话,有 dp[1]中方法 凑成 容量为5的背包
  • 已经有一个5 (nums[i])的话,有 dp[0]中方法 凑成 容量为5的背包

那么凑整dp[5]有多少方法呢,也就是把 所有的 dp[j - nums[i]] 累加起来

初始化:

首先dp[0]一定要为1,dp[0] = 1是 递归公式的基础。如果dp[0] = 0 的话,后面所有推导出来的值都是0了。下标非0的dp[j]初始化为0,这样累计加dp[j - coins[i]]的时候才不会影响真正的dp[j]。dp[0]=1还说明了一种情况:如果正好选了coins[i]后,也就是j-coins[i] == 0的情况表示这个硬币刚好能选,此时dp[0]为1表示只选coins[i]存在这样的一种选法。

组合不强调元素之间的顺序,排列强调元素之间的顺序

遍历顺序:

本题要求凑成总和的组合数,元素之间明确要求没有顺序。所以纯完全背包是能凑成总和就行,不用管怎么凑的。本题是求凑出来的方案个数,且每个方案个数是为组合数。那么本题,两个for循环的先后顺序可就有说法了。

如果求组合数就是外层for循环遍历物品,内层for遍历背包

假设:coins[0] = 1,coins[1] = 5。

那么就是先把1加入计算,然后再把5加入计算,得到的方法数量只有{1, 5}这种情况。而不会出现{5, 1}的情况。

所以这种遍历顺序中dp[j]里计算的是组合数!

如果求排列数就是外层for遍历背包,内层for循环遍历物品

背包容量的每一个值,都是经过 1 和 5 的计算,包含了{1, 5} 和 {5, 1}两种情况。

此时dp[j]里算出来的就是排列数!

代码:

class Solution {
    public int change(int amount, int[] coins) {
        //递推表达式
        int[] dp = new int[amount + 1];
        //初始化dp数组,表示金额为0时只有一种情况,也就是什么都不装
        dp[0] = 1;
        for (int i = 0; i < coins.length; i++) {
            for (int j = coins[i]; j <= amount; j++) {
                dp[j] += dp[j - coins[i]];
            }
        }
        return dp[amount];
    }
}

 

三、377. 组合总和 Ⅳ 

推导基本上与上一题没有什么区分,不同的是,零钱是组合,不强调顺序;本题是排列,强调顺序;因此 区别在于本题先遍历背包后遍历物品,这样使得顺序出现区别。

class Solution {
    public int combinationSum4(int[] nums, int target) {
        int[] dp = new int[target + 1];
        dp[0] = 1;
        for (int i = 0; i <= target; i++) {
            for (int j = 0; j < nums.length; j++) {
                if (i >= nums[j]) {
                    dp[i] += dp[i - nums[j]];
                }
            }
        }
        return dp[target];
    }
}

 

四、爬楼梯(进阶版)

一步一个台阶,两个台阶,三个台阶,.......,直到 m个台阶。问有多少种不同的方法可以爬到楼顶呢?

1阶,2阶,.... m阶就是物品,楼顶就是背包。

每一阶可以重复使用,例如跳了1阶,还可以继续跳1阶。

问跳到楼顶有几种方法其实就是问装满背包有几种方法。

此时大家应该发现这就是一个完全背包问题了!

本题看起来是一道简单题目,稍稍进阶一下其实就是一个完全背包!

如果我来面试的话,我就会先给候选人出一个 本题原题,看其表现,如果顺利写出来,进而在要求每次可以爬[1 - m]个台阶应该怎么写。

顺便再考察一下两个for循环的嵌套顺序,为什么target放外面,nums放里面。

这就能考察对背包问题本质的掌握程度,候选人是不是刷题背公式,一眼就看出来了。

这么一连套下来,如果候选人都能答出来,相信任何一位面试官都是非常满意的。

本题代码不长,题目也很普通,但稍稍一进阶就可以考察完全背包,而且题目进阶的内容在leetcode上并没有原题,一定程度上就可以排除掉刷题党了,简直是面试题目的绝佳选择!

class Solution {
    public int climbStairs(int n) {
        int[] dp = new int[n + 1];
        int m = 2; //有兩個物品:itme1重量爲一,item2重量爲二
        dp[0] = 1;

        for (int i = 1; i <= n; i++) { // 遍历背包
            for (int j = 1; j <= m; j++) { //遍历物品
                if (i >= j)  //當前的背包容量 大於 物品重量的時候,我們才需要記錄當前的這個裝得方法(方法數+)
			dp[i] += dp[i - j];
            }
        }

        return dp[n];
    }
}

 


总结

完全背包;

你可能感兴趣的:(算法)