2个表 遍历 组合_动态规划求解“组合总和”问题

组合总和 - 力扣(LeetCode)​leetcode-cn.com
feb90432ac332d6df2265c9859af0814.png

该系列题目一共有4道,题目的难度递增,以上是第1道的链接,这里先贴一下该题目的描述:

给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。candidates 中的数字可以 无限制重复被选取。
说明:所有数字(包括 target)都是正整数。解集 不能包含重复的组合。
例 1: 输入: candidates = [2,3,6,7], target = 7, 所求解集为:
[ [7], [2,2,3] ]
示例 2: 输入: candidates = [2,3,5], target = 8, 所求解集为:
[ [2,2,2,2], [2,3,3], [3,5] ]
class Solution:
    def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:

一般而言,可以使用回溯法解决这个问题,首先遍历candidates,将target挨个减去其中的值,得到一个新的sub_target,然后把sub_target递归求解,直到子问题的target小于0

class Solution:
    def combinationSum(self, candidates: list, target: int) -> list:
        if target == 0: return [[]]
        if target < 0: return []
        res = []
        for num in candidates:
            sub_target = target - num
            sub_res = self.combinationSum(candidates, sub_target)
            res += [[num]+comb for comb in sub_res]
        return res

例如candidates = [2,3,5], target = 8,通过递归搜索可以形成一个如下图所示的“树”:

f6a31e08126c066afc0be47c63d8fc17.png

上图并不完全,有的明显不可能的结果就不画出来了。这个算法虽然简单,但是存在一个问题就是结果重复,例如本例的输出是:

[[5, 3], [2, 2, 2, 2], [2, 3, 3], [3, 5], [3, 2, 3], [3, 3, 2]]

[5, 3], [3, 5] 与 [2, 3, 3], [3, 2, 3], [3, 3, 2] ,这两组答案都发生了重复,当然还有那些不能够组成答案的组合也有重复,如下图:

8f868f5eb733160a5dcffeedd266ca61.png
同色框中的计算都重复了

仔细考虑前面的代码,其实不需要每次都将candidates完全遍历一遍,举个具体的栗子:当有了答案 [2, 3, 3] 时,就不需要[3, 2, 3]和[3, 3, 2]了,所以只要将传给下级递归函数的candidate中的元素2给移除就可以了,具体代码将

for num in candidates:
    sub_target = target - num
    sub_res = self.combinationSum(candidates, sub_target)

替换为

for i, num in enumerate(candidates):
    sub_target = target - num
    sub_res = self.combinationSum(candidates[i:], sub_target)

将代码修改后,递归树变为下面的样子:

d889ef5c0a550ec264147ebe4b665a33.png

从上图可以看出,重复的答案都不会被递归到了。最后,再将代码写简洁一点:

class Solution:
    def combinationSum(self, candidates: list, target: int) -> list:
        if target < 0: return []
        if target == 0: return [[]]
        return [[num]+comb for i, num in enumerate(candidates) for comb in self.combinationSum(candidates[i:], target - num)]

这个系列的第2,3题也能用回溯解决,但是第4题会超时,因为这种回溯有重复的计算,会降低效率,通常可以采用带备忘录的递归来消除重复的计算,简单的说就是将每一个target的组合结果保存下来,下次递归碰到相同值的target时直接返回之前计算的结果即可,这里就不展开了,下面讨论动态规划的方法求解。

在正式讨论动态规划的解法之前,首先引入一个概念:状态转移方程

什么是状态转移方程?直接看一个经典的栗子:斐波那契数列。它的的状态转移方程为:

根据该方程,我们可以轻松的写出简洁高效的代码

def feb(n):
    A = [0] + [1] * n
    for i in range(1, n+1):
        A[i] = A[i-1] + A[i-2]
    return A[n]

不过使用动态规划还有一些条件(如:最优子结构性质,子问题的重叠性等),在前面给出的链接中有说明,也可以自行百度查找更详细的讲解

回到我们的问题,如果现在直接把方程贴上来的话,估计过一段时间我自己也看不明白了,所以为了更详细的说明这个问题的求解过程,先简单的了解一下另一个动态规划的问题:01背包问题,该问题的简单描述如下:

现在有

件物品,每一件物品有它的
价值重量(我们将第
件物品的价值用
表示,重量用
表示),现给定一个背包,限制总重量为
,问:要往背包里装哪些物品才能实现总价值的最大化?例如:

c14badbf4837c7360b0ae9b7f35b632b.png

为了解决这个问题,我们定义一个二维数组

,用来存放若干件物品中限定重量的所能达到的最大价值,例如,
表示前
件物品中,限制重量为
时,所能达到(放进背包)的最大价值。首先,当
时,显然
,所以我们可以得到状态转移方程的初始条件

假设我们已经遍历了前

件物品,现在第
件物品摆在我们面前,有个明显的限制就是当这件物品的重量超过背包的总重量,即
时,就算这件物品的价值再大也要不起,所以,又可以往方程中加一行:

之前已经说过,我们遍历了前

件物品,这意味着我们不仅仅是在重量限制为
的条件下遍历了这些物品,而是在重量限制为
的条件下都遍历了一遍(不然为什么
是二维数组呢)

接着,如果第

件物品的重量小于限制(
),这时候就需要考虑该物品的价值是否值得从背包中腾出
的物品出来,简而言之就是比较:当前背包中的价值
,和该物品的价值加上腾出来以后背包的价值
,最终得到状态转移方程为:

以上就是01背包问题动态规划解法的状态转移方程,代码就不展开了。之所以要先介绍这个问题是因为它和我们的题目很相似,我们也需要通过一个二维数组来解决这个问题,先来回顾一下问题:

给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合

为了解决这个问题,我们定义一个二维数组

,它的一个具体的元素,例如
表示 candidates 前
个元素中所有和为
的组合(也就是说
是一个包含着集合的集合,对应Python中就是包含列表的列表),而
就是问题的答案(length 为 candidates 的长度)

首先我们从一个具体的题目入手,还是用 candidates = [2, 3, 5], target = 8 来举例。先确定初始条件,显然当

时没有这种组合,所以:

然后是跟前面讨论的01背包问题同样的套路,假设已经遍历了前

个元素,接着尝试(candidates的)第
个元素(用
表示),如果
,显然不可能用
组合成
(例如构成 2 的组合不可能包含 3)

并且

包含
的所有组合,也就是说, candidates 前
个元素和为
的组合显然包含前
个元素和为
的组合,所以将方程更新一下:

上面

是一个集合,是加入第
个元素后所能构成的新的组合的集合,例如
只包含
这一个组合,但是当加入第2个元素(即3)后,

将它们代入方程,可以得到本例中

,注意
不是
,那么
的通式该如何确定?首先可以肯定的是
中的组合(如果
)肯定都包含了
,因为不包含
而和又为
的组合已经全部被
给笑纳了

是一个集合,它里面的
每一个元素也是一个集合,于是就有
,存在
。简单的说就是
所包含的每一个集合中,都含有一个元素
,如果我们把这个
包含的所有集合中去掉,那不就变成了
吗?

例如前面栗子中

,我们从中去掉一个3(
),就得到了
,也就是
,所以我们
暂时可以得到一个通式:

之所以说暂时,是因为还有2个条件需要补充,为了说明这一点,先看一个具体栗子

d6aa56096d2ecbd84d1f26d880458bfd.png
i=1时

从我们前面定义的方程可知

然后据此来计算
,因为
,光用2能组成哪些值我想并不难算,如上表所示。根据前面
的通式,可以验证一下:

,所以得到
,再加上
,最终得到:

嗯,这个没有问题,再来验证一个

,所以
,再并上
,最终还是一个空集。嗯,没毛病

那么问题在哪呢?我们可以试着从

一直往上追溯,直到计算

因为

,所以
,再加上
,最终得到:

不对啊,

明明应该是
的呀,怎么会是空呢,难道2不能组成2吗?

所以,为了解决这一问题,我们单独的令

,这样就能得到
,那么再来计算

继而得到

,从而
也就稳了,最后得到的状态转移方程就是:

然后,就是根据方程敲代码了,因为Python的列表可以实现并集,所以就只需要

个列表即可,而不用
个列表,关于
的初始化
A = [[[]]] + [[] for i in range(target)]

上面的语句是将A[0]初始化为[[]],而A的其他元素则是一个空列表,最终的代码如下:

def combinationSum(self, candidates: list, target: int) -> list:
    A = [[[]]] + [[] for i in range(target)]
    for c in candidates:
        for n in range(c, target+1):      #1
            A[n] += [comb+[c] for comb in A[n-c]]     #2
    return A[target]

关于注释为#1的那句代码保证了

,而
[comb+[c] for comb in A[n-c]] 得到的结果就是

以上,是关于“组合总和”系列题目的第1道题,因为本篇笔记的目的在于讨论动态规划,题目只是一种手段,是用来说明动态规划求解过程的一个工具。由于第2,3题与第1题太相似(第2题等于第1题换了个马甲,而第3题又是第2题换了个马甲),所有就不讨论了,只把链接给出来,感兴趣的可以看一看,下面直接讨论第4题

组合总和 II - 力扣(LeetCode)​leetcode-cn.com
feb90432ac332d6df2265c9859af0814.png
组合总和 III - 力扣(LeetCode)​leetcode-cn.com
feb90432ac332d6df2265c9859af0814.png

最后,再借助第4题来讨论动态规划方法

组合总和 Ⅳ - 力扣(LeetCode)​leetcode-cn.com
feb90432ac332d6df2265c9859af0814.png

首先,贴出题目:

给定一个由正整数组成且不存在重复数字的数组,找出和为给定目标正整数的组合的 个数
示例: nums = [1, 2, 3], target = 4
所有可能的组合为:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)
请注意,顺序不同的序列被视作不同的组合。因此输出为 7。

这一道题看起来和第1题神似,套用第1题的套路也能求出来,但是会有冗余,速度慢了一个量级,在这里先把最终的状态转移方程给出来,然后再来看看是如何推导出来的

表示 nums 中能构成和为
的组合的个数,而
是 nums 中所有小于
的元素

下面,我们通过一个栗子试着归纳该问题的规律,设 nums = [1,2,3], target = 5,通过穷举法,我们可以得到下表:

e72764245059d526f0b7e0e454ddf6c0.png

上图第一列是

的值,第二列是所有的组合情况,第三列是对应的组合的数量,接着增加第四、五、六列

12a40ee971577aac5f8d94b25a779e3e.png

看到这里你可能找到感觉了,接着把整个表列出来

e369dba48f05ffdcf425cdae78f5cec3.png

这里的逻辑是,当知道

的值后,而

但是因为必须由 nums 中的元素所组成,所以

是 nums 中小于
的元素,当我们知道
的组合数量后,在每一种组合的基础上,再并入元素
,就得到了
一部分组合,所以可以得到:

有一点要注意的是,当

时,
自己就是构成
的一个组合,着就是
的原因。综上,我们就推出了前面给出的状态转移方程

根据该方程,可以轻易写出响应代码:

def combinationSum4(self, nums: list, target: int) -> int:
    A = [1] + [0] * target
    for n in range(1, target+1): 
        A[n] = sum([A[n-c] for c in nums if n >= c])
    return A[target]
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值