目录
LeetCode39. 组合总和
1. 思路
本题和 组合,组合III的两个不同点是:
- 组合没有数量要求
- 元素可以无限制的被重复选取
关于题目条件的思考
- 题目中的无限制重复被选取,吓得我赶紧想想 出现0 可咋办,然后看到下面提示:1 <= candidates[i] <= 200,我就放心了;
- 不用考虑去重问题,因为数组中的每个元素都互不相同
本题虽然说是组合没有数量要求,元素也可以无限重复,但是有总和的限制,所以间接地也是有元素个数的限制;
本题搜索的过程抽象成树形结构如下:
注意图中叶子节点的返回条件,因为本题没有组合数量要求,仅仅是总和的限制,所以递归没有层数的限制,只要选取的元素总和超过target,就返回!
而在77.组合和216.组合总和III中都可以知道要递归K层,因为要取k个元素的组合
为什么取5的下面是在【5,3】中取,而不是【2,5,3】?
因为组合不强调元素顺序,如果这样写的话会出现重复的组合,比如【2,3】和【3,2】;
2. 代码实现
回溯三部曲
2.1 递归函数参数
这里依然是定义两个全局变量,二维数组result存放结果集,数组path存放符合条件的结果。(这两个变量也可以作为函数参数传入,但这里不这么做);
首先是题目中给出的参数,集合candidates, 和目标值target。
此外我还定义了int型的sum变量来统计单一结果path里的总和,其实这个sum也可以不用,用target做相应的减法就可以了,最后如何target==0就说明找到符合的结果了,但为了代码逻辑清晰,我依然用了sum。
本题还需要startIndex来控制for循环的起始位置,对于组合问题,什么时候需要startIndex呢?
-
如果是一个集合来求组合的话,就需要startIndex,例如:77.组合 ,216.组合总和III ;
-
如果是多个集合取组合,各个集合之间相互不影响,那么就不用startIndex,例如:17.电话号码的字母组合;
注意以上我只是说求组合的情况,如果是排列问题,又是另一套分析的套路,后面我再讲解排列的时候就重点介绍;
class Solution:
def __init__(self):
self.path = []
self.paths = []
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
'''
def backtracking(self, candidates: List[int], target: int, sum_: int, start_index: int) -> None:
2.2 递归终止条件
在以上树形结构中,从叶子节点可以清晰看到,终止只有两种情况,sum大于target和sum等于target;sum等于target的时候,需要收集结果,代码如下:
# Base Case
if sum_ == target:
# 因为是shallow copy,所以不能直接传入self.path
self.paths.append(self.path[:])
return
if sum_ > target:
return
2.3 单层搜索的逻辑
单层for循环依然是从startIndex开始,搜索candidates集合。
注意本题和77.组合 ,216.组合总和III 的一个区别是:本题元素为可重复选取的;如何重复选取呢?在向下一层递归函数的时候,不写i+1了,直接写i,表示一个重复读取当前的数;
# 单层递归逻辑
for i in range(start_index, len(candidates)):
sum_ += candidates[i]
self.path.append(candidates[i])
# 因为无限制重复选取,所以不是i+1
self.backtracking(candidates, target, sum_, i)
sum_ -= candidates[i] # 回溯
self.path.pop() # 回溯
2.4 整体代码如下
class Solution(object):
def __init__(self):
self.path = []
self.result = []
def combinationSum(self, candidates, target):
"""
:type candidates: List[int]
:type target: int
:rtype: List[List[int]]
"""
if candidates == []: return []
self.traversal(candidates,target,0,0)
return self.result
def traversal(self,candidates,target,curSum,startIndex):
# 终止条件
if curSum == target:
# 因为是shallow copy,所以不能直接传入self.path
self.result.append(self.path[:])
return
if curSum > target:
return
# 单层回溯搜索
for i in range(startIndex,len(candidates)):
self.path.append(candidates[i])
curSum += candidates[i]
# 因为无限制重复选取,所以不是i+1
self.traversal(candidates,target,curSum,i)
self.path.pop()
curSum -= candidates[i]
3. 剪枝优化
在这个树形结构中:
以及上面的代码大家可以看到,对于sum已经大于target的情况,其实是依然进入了下一层递归,只是下一层递归结束判断的时候,会判断sum > target的话就返回。
其实如果已经知道下一层的sum会大于target,就没有必要进入下一层递归了;那么可以在for循环的搜索范围上做做文章了。对总集合排序之后,如果下一层的sum(就是本层的 sum + candidates[i])已经大于target,就可以结束本轮for循环的遍历;
for循环剪枝代码如下:
# 别忘了在主函数里面先排序
for i in range(start_index, len(candidates)):
if sum_ + candidates[i] > target:
return
剪枝之后整体代码如下:
class Solution(object):
def __init__(self):
self.path = []
self.result = []
def combinationSum(self, candidates, target):
"""
:type candidates: List[int]
:typ