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

 

前言

动态规划问题中,完全背包问题一直都是一个非常棘手的问题。这类问题大致的描述如下:背包总容量 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]
  • 9
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值