文章目录

题目描述

给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用 一次 。
注意:解集不能包含重复的组合。

示例 1:
输入: candidates = [10,1,2,7,6,1,5], target = 8,
输出:
[
[1,1,6],
[1,2,5],
[1,7],
[2,6]
]

示例 2:
输入: candidates = [2,5,2,1,2], target = 5,
输出:
[
[1,2,2],
[5]
]

思路分析

这道题是 ​​39. 组合总和​​ 的进化版,先做39再做这个比较好。

39题不需要去重,这道题升级了一下,需要去重了,虽然只加了两行代码,不过这里面很难理解。

老规矩,回溯三步走:

1.确定函数参数

首先就是题目所给的target ,然后是我们计算的sum,走过的路径的节点也得记录一下用path,答案集合也得记录一下用res,开始下标值也得记录一下用startindex。

def backtrack(target,sum,startindex,path):

2.确定终止条件
这到题的终止条件和上一道题一样,
sum大于target时,直接return。
和sum等于target时,先将当前结果加入答案集,然后再return。

if sum > target:
return
if sum == target:
res.append(path[:])

3.确定循环体:

首先是剪枝,当前的sum和即将递归的数值和如果大于目标值了,可以直接return掉。

然后就是最难的去重了,这里说一下利用startindex去重的方法,当然也有用数组记录去重的,那个比较容易理解就不说了。

首先要明白一点,就是startindex和for循环里的变量i分别代表着什么。

来个例子,假如初始数组[1,1,2],target=3,此时进入​​for i in (startindex,len(candidates ))​

  • 这时候的i和startindex都是0,单个分支下取第二个1之后的startindex 和 i 都是1。
  • 再取2,此时的i和startindex都是2,sum已经等于4大于target了,那么开始回溯。
  • 回溯到上一层,startindex和i都是1的时候,此时不取第二个1了,开始取2,此时的startindex为1,i为2。

拿文字描述比较乱,这块可以直接画个树就能明白了。

这块理解起来比较抽像,因为startindex是i的初始值,当横向遍历完之后,i会+1,而startindex没变,只有在单个分支纵向递归或者回溯的时候,startindex才加或者减。 可以片面的理解成,startindex控制纵向遍历的起始点,i控制横向遍历的下标。

这也是为什么这道题在回溯的过程中需要传i+1,而上一道题不需要+1的原因。(上一道题可以在单个组合中重复使用一个元素,这道题里不能。startindex控制纵向遍历起始点,一个组合中不能重复使用,所以往下传的时候+1,即只能使用本元素之后的元素。)

总结下来就是,在单个分支下的回溯 startindex始终<=i。 如果切换了分支 则i>startindex。

为什么要判断他是不是切换分支了?

其实是本题的要求,例子还是[1,1,2],你可以 ----第一个1和第二个1组成[1,1],因为他们使用了不同 1,只是值相同而已,这里显然是取第一个1下面的分支的操作过程,所以在一个组合里,元素是可以相同的,而如果你取的是第一个1和2,以及第二个1和2,这样就以为着有两个相同的组合 [1,2]了,虽然用的不是同一个1,但题目不允许这样。 这时候就是切分支带来的重复结果,所以我们要判断是不是切分支了,如果没切分支,重复是可以的,因为一个分支最终的叶子就是一个组合结果。

即 组合内可以有重复值,组合间不能有整体重复。
所以当 candidates[i] == candidates[i-1] 时候,可以判断重复了,i > startindex 可以判断切分支了,所以此时要进行去重了。

这块去重不能用return,毕竟后面还要继续判断呢,直接不处理他,continue就行了。

if sum + candidates[i] > target:
return # 剪枝

if i > startindex and candidates[i] == candidates[i-1]:# 去重
continue
path.append(candidates[i])
sum += candidates[i]
backtrack(target,sum,i+1,path)
path.pop()
sum -= candidates[i]

完整代码

class Solution:
def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
res = []
path = [] # 存路径节点
candidates.sort()
def backtrack(target,sum,startindex,path):
# 确定终止条件
if sum > target:
return
if sum == target:
res.append(path[:])
# 递归体
for i in range(startindex,len(candidates)):
if sum + candidates[i] > target:
return # 剪枝
# 去重
if i > startindex and candidates[i] == candidates[i-1]:
continue
path.append(candidates[i])
sum += candidates[i]
backtrack(target,sum,i+1,path)
path.pop()
sum -= candidates[i]
backtrack(target,0,0,path)
return