代码随想录算法训练营第四十四天 | 完全背包理论基础、518. 零钱兑换 II、377. 组合总和 Ⅳ

57 阅读2分钟

完全背包理论基础

文章链接

理论基础

由于之前说过0-1背包的理论基础(滚动数组解法),所以很容易就知道完全背包的写法,完全背包的含义是每个物品都可以用多次,代码如下:

function foo(weight: number[], value: number[], size: number) {
  let dp: number[] = new Array(size + 1).fill(0)
  for (let i = 0; i < weight.length; i++) {
    for (let j = weight[i]; j <=size; j++) {//这里为正序 就可以一个物品使用多次
      dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i])
    }
  }
  console.log(dp)
  console.log(dp[size])
}
foo([1, 3, 4], [15, 20, 30], 4)

同时,文章也讲解了遍历顺序的问题,如果是纯完全背包问题,则两层的遍历顺序是可以转变的,可以先遍历背包,也可以先遍历物品,下面是先遍历物品的图(行遍历):

image.png 先遍历背包的图(列)

image.png

518. 零钱兑换 II

链接

文章链接

题目链接

第一想法

这道题看完题后,发现和目标和这道题是挺类似的,不一样的是这道题每个元素可以无穷使用,那么接下来递归五部曲

  1. dp数组的含义,dp[i]为和为i的组合数
  2. 初始化,其他的都为0,只有dp[0]=1,原因是让递推公式可以顺利递推,防止递推公式的结果都是0
  3. 递推公式,和目标和一样dp[j] += dp[j - coins[i]] 原因在目标和中写了 例如和为5的组合数等于元素为1时(和为4的组合数)+元素为2时(和为3的组合数)+....元素为4时(和为1的组合数)之和
  4. 遍历顺序 还是使用的是先物品再背包
function change(amount: number, coins: number[]): number {
  let dp: number[] = new Array(amount + 1).fill(0)
  dp[0] = 1
  for (let i = 0; i < coins.length; i++) {
    for (let j = coins[i]; j <= amount; j++) {
      dp[j] += dp[j - coins[i]]//组合数相加求解
    }
  }
  return dp[amount]
}

看完文章后的想法

文章的想法和我的想法是一致的,但是! 文章的讲解中使我发现我没有考虑到的,我这里是先物品再背包,所以能出现正确答案,但是先背包再物品的话是不可以的!!

例如:coins[1,3],amount=4,先物品再背包得到结果为

dp容量0容量1容量2容量3容量4
coins[0]11111
coins[1]11122

这样结果组合为4只包含{1,1,1,1}和{1,3}两种情况

如果先遍历背包再物品的话,代码为:

  for (let j = 0; j <= amount; j++) { // 遍历背包容量
    for (let i = 0; i < coins.length; i++) { // 遍历物品
        if (j - coins[i] >= 0) dp[j] += dp[j - coins[i]];
    }
}

结果为(列为背包遍历,行为dp数组的值)

dp容量0容量1容量2容量3容量4
容量010000
容量111000
容量211100
容量311120
容量411123

这样结果组合为4只包含{1,1,1,1}和{1,3}和{3,1}三种情况,所以先物品再背包像是组合,先背包再物品像是排列

思考

这道题能写出来,但是,对于先背包还是先物品会导致结果的不同我是没想到的,通过文章的讲解我发现了先物品就是想组合,而先背包就像排列

377. 组合总和 Ⅳ

链接

文章链接

题目链接

第一想法

刚刚在零钱兑换II这道题解释了先遍历背包和先遍历物品的不同,所以这道题很轻松的就写出来了,递归五部曲为:

  1. dp数组的含义,dp[j]表示和为j的组合数
  2. 初始化,还是dp[0]=1,其他为0
  3. 递推公式:dp[j] += dp[j - nums[i]]
  4. 遍历顺序,这道题由题可知,不同的排列算不同的组合,所以先遍历背包
function combinationSum4(nums: number[], target: number): number {
  let dp: number[] = new Array(target + 1).fill(0)
  dp[0] = 1
  for (let j = 0; j <= target; j++) {
    for (let i = 0; i < nums.length; i++) {
      if (j >= nums[i]) dp[j] += dp[j - nums[i]]
    }
  }
  return dp[target]
}

看完文字后的想法

文章的想法我上面已经说了,所以这里就不加赘述了,下面附一张代码随想录中的图来解释一下先遍历背包的情况(为什么是排列)

image.png

思考

这道题之前在零钱兑换 II这道题中已经想清楚了,所以这道题很快就想到用排列而不是组合,所以困难。

今日总结

今日学习了完全背包理论,因为再0-1背包滚动数组的解法中就了解过,所以就比较好理解今天的内容。同时了解了先遍历背包和先遍历物品和是组合还是排列问题有关,今日耗时2.5小时