![feb90432ac332d6df2265c9859af0814.png](https://i-blog.csdnimg.cn/blog_migrate/dc4cec3a871c5b309bee269d2dc618fa.jpeg)
该系列题目一共有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](https://i-blog.csdnimg.cn/blog_migrate/8f6cc2aec7d65c7f938bc2b292a17952.jpeg)
上图并不完全,有的明显不可能的结果就不画出来了。这个算法虽然简单,但是存在一个问题就是结果重复,例如本例的输出是:
[[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](https://i-blog.csdnimg.cn/blog_migrate/d2260f86507ec2d96e3acf28f66aa4f6.jpeg)
仔细考虑前面的代码,其实不需要每次都将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](https://i-blog.csdnimg.cn/blog_migrate/456ba1dcefa4819c9b6951c62a30ed01.jpeg)
从上图可以看出,重复的答案都不会被递归到了。最后,再将代码写简洁一点:
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](https://i-blog.csdnimg.cn/blog_migrate/e87fecfb3d5cf2e1fbd0073b175f7e98.png)
为了解决这个问题,我们定义一个二维数组
假设我们已经遍历了前
之前已经说过,我们遍历了前
接着,如果第
以上就是01背包问题动态规划解法的状态转移方程,代码就不展开了。之所以要先介绍这个问题是因为它和我们的题目很相似,我们也需要通过一个二维数组来解决这个问题,先来回顾一下问题:
给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合
为了解决这个问题,我们定义一个二维数组
首先我们从一个具体的题目入手,还是用 candidates = [2, 3, 5], target = 8 来举例。先确定初始条件,显然当
然后是跟前面讨论的01背包问题同样的套路,假设已经遍历了前
并且
上面
将它们代入方程,可以得到本例中
且
例如前面栗子中
之所以说暂时,是因为还有2个条件需要补充,为了说明这一点,先看一个具体栗子
![d6aa56096d2ecbd84d1f26d880458bfd.png](https://i-blog.csdnimg.cn/blog_migrate/b5753285bd7b98548e23095f985cb1d7.jpeg)
从我们前面定义的方程可知
而
嗯,这个没有问题,再来验证一个
而
那么问题在哪呢?我们可以试着从
因为
不对啊,
所以,为了解决这一问题,我们单独的令
继而得到
然后,就是根据方程敲代码了,因为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的那句代码保证了
以上,是关于“组合总和”系列题目的第1道题,因为本篇笔记的目的在于讨论动态规划,题目只是一种手段,是用来说明动态规划求解过程的一个工具。由于第2,3题与第1题太相似(第2题等于第1题换了个马甲,而第3题又是第2题换了个马甲),所有就不讨论了,只把链接给出来,感兴趣的可以看一看,下面直接讨论第4题
组合总和 II - 力扣(LeetCode)leetcode-cn.com![feb90432ac332d6df2265c9859af0814.png](https://i-blog.csdnimg.cn/blog_migrate/dc4cec3a871c5b309bee269d2dc618fa.jpeg)
![feb90432ac332d6df2265c9859af0814.png](https://i-blog.csdnimg.cn/blog_migrate/dc4cec3a871c5b309bee269d2dc618fa.jpeg)
最后,再借助第4题来讨论动态规划方法
组合总和 Ⅳ - 力扣(LeetCode)leetcode-cn.com![feb90432ac332d6df2265c9859af0814.png](https://i-blog.csdnimg.cn/blog_migrate/dc4cec3a871c5b309bee269d2dc618fa.jpeg)
首先,贴出题目:
给定一个由正整数组成且不存在重复数字的数组,找出和为给定目标正整数的组合的 个数。
示例: 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 = [1,2,3], target = 5,通过穷举法,我们可以得到下表:
![e72764245059d526f0b7e0e454ddf6c0.png](https://i-blog.csdnimg.cn/blog_migrate/1a4b7097acb54d3b2d5a028e832884df.jpeg)
上图第一列是
![12a40ee971577aac5f8d94b25a779e3e.png](https://i-blog.csdnimg.cn/blog_migrate/0952d9841b160fc198fcae3290c8be73.jpeg)
看到这里你可能找到感觉了,接着把整个表列出来
![e369dba48f05ffdcf425cdae78f5cec3.png](https://i-blog.csdnimg.cn/blog_migrate/0cf4ab9074a38b4000f064f7a4c76d3a.jpeg)
这里的逻辑是,当知道
但是因为必须由 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]