关于完全背包问题求解组合数与排列数编码差别

您所在的位置:网站首页 排列组合类型题和解析题一样吗 关于完全背包问题求解组合数与排列数编码差别

关于完全背包问题求解组合数与排列数编码差别

2024-07-14 20:23| 来源: 网络整理| 查看: 265

  前言

动态规划问题中,完全背包问题一直都是一个非常棘手的问题。这类问题大致的描述如下:背包总容量 j 一定,物品具有 weight[i], value[i] 的属性,问在每种物品数量无限的情况下,如何使得最后背包内的总价值最大。这类问题用暴力枚举的时间复杂度量级达到指数级别(每种物品都有取与不取两种情况),肯定是不可取的,较常见的方法是使用动态规划的方式来解题,动态规划的时间复杂度为 O(m*n)(其中m表示背包容量,n表示物品种类)。我主要想讲的是,当最后完全背包问题是求解方案组合数与排列数时,编码的差异。

问题来源

518. 零钱兑换 II

讨论

由于题目中说了,每一种面额的硬币有无限个,所以这道题是一道比较明显的完全背包问题。下面我主要想区分一下当要求输出排列数或组合数时,这道题(其他完全背包问题也适用)的不同编码方式。

dp[j]:表示凑成总金额为 j 的硬币组合数为 dp[j]。

递推公式推导: 我们可以想到,假如题目要求凑成总金额为 5 的组合数,硬币种类为 [1, 2, 5],那么我们不难想到,组合数就是 dp[4] + dp[3] + dp[0],怎么得出的呢?我将组合数这样排列其实是有目的的,由于硬币种类为3种,那么凑成金额 5 的方案有如下三种情况:

凑成总金额为 4 的方案 + 硬币 1 凑成总金额为 3 的方案 + 硬币 2 凑成总金额为 0 的方案 + 硬币 5

不知道大家能不能看出来,其实我们每次需要累加的就是 dp[j - coin[i]]。或许有人会问,那我方案里重复使用一种硬币是从哪里体现。我们从上面的三种方案里看方案一,虽然后面只是 +硬币1,但其实前面的 凑成总金额为 4 的方案 内已经包含了重复使用 硬币 1 的情况(例如硬币组合[1,1,1,1,1]),所以我们只需要考虑当前循环情况下:遍历硬币种类,不同的硬币种类再加上可以组成总金额的 金额为 j - coin[i] 的方案数。那么,我们递推公式就可以顺理成章的得出了:

dp[j] += dp[j - coin[i]]

而这个递推公式,也是解完全背包问题时,求装满背包有几种方案的一般公式。 我的标题里讲了,要区别排列数与组合数。我们知道,排列数是要考虑顺序的,组合数是不需要考虑顺序的。这一题就是不需要考虑顺序的求解组合数的情况。那么我们面对完全背包问题时,什么时候需要考虑顺序呢?以及,考虑顺序和不考虑顺序时,编码方式有什么不同呢?

举一个需要考虑顺序的完全背包问题情况:

例如,小p知道一串电话号码的所有数字和为 100,但是小p不知道该电话号码的总位数,也不知道里面任何一位数字,题目只给了电话号码数字范围为 [0, 9],求总共有几种电话号码组合?

这道题虽然问的是组合,但是我们都知道电话号码 123 与 321 是两种不同的组合,那么这道题就是一道需要考虑顺序的完全背包问题了。

考虑顺序和不考虑顺序的编码方式有什么不同呢?

不考虑顺序:

for coin in coins:     for j in range(coin, amount + 1):         dp[j] += dp[j - coin]

考虑顺序:

for j in range(amount + 1):     for coin in coins:         if j - coin >= 0: dp[j] += dp[j - coin]

可以看出,不考虑顺序是先遍历的硬币种类,再遍历的背包容量,而考虑顺序是先遍历的背包容量,再遍历的硬币种类。我们举一个例子就可以看出二者差别了: 假设硬币种类为 [1, 2, 3],目标背包容量 amount = 3 时:

不考虑顺序的遍历方式: dp[0] = 1 (不在背包内放东西也算一种方案) coin = 1: j = 1: dp[1] += dp[1 - 1] = dp[0] = 1 j = 2: dp[2] += dp[2 - 1] = dp[1] = 1 j = 3: dp[3] += dp[3 - 1] = dp[2] = 1 coin = 2: j = 2: dp[2] += dp[2 - 2] = dp[0] = 2 j = 3: dp[3] += dp[3 - 2] = dp[1] = 2 coin = 3: j = 3: dp[3] += dp[3 - 3] = dp[0] = 3

考虑顺序的遍历方式: dp[0] = 1 j = 1: coin = 1: dp[1] += dp[1 - 1] = dp[0] = 1 j = 2: coin = 1: dp[2] += dp[2 - 1] = dp[1] = 1 coin = 2: dp[2] += dp[2 - 2] = dp[0] = 2 j = 3: coin = 1: dp[3] += dp[3 - 1] = dp[2] = 2 coin = 2: dp[3] += dp[3 - 2] = dp[1] = 3 coin = 3: dp[3] += dp[3 - 3] = dp[0] = 4

上面是两种编码方式的在 j=3 时逻辑推导过程,那么差别到底在哪里呢?面对容量为3的背包,不考虑顺序的编码,方案为:

金额和为2的由硬币1构成的子方案 + 硬币1; 金额和为1的由硬币1&2构成的子方案 + 硬币2; 金额和为0的由硬币1&2&3构成的子方案 + 硬币3;

而考虑顺序的编码,在 j = 3 时,硬币方案为:

金额和为2的由硬币1&2&3构成的子方案 + 硬币1; 金额和为1的由硬币1&2&3构成的子方案 + 硬币2; 金额和为0的由硬币1&2&3构成的子方案 + 硬币3;

也就是说,不考虑顺序的编码过程中,子方案金额数不能超过硬币面值,但是,考虑顺序的编码时,子方案金额数可以超过硬币面值!! 而这就是两种编码方式的重要区别!

这道题的代码:

class Solution: def change(self, amount: int, coins: List[int]) -> int: dp = [0]*(amount + 1) dp[0] = 1 for coin in coins: for i in range(coin, amount + 1): dp[i] += dp[i - coin] return dp[amount]


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3