leetcode(力扣) 40. 组合总和 II(回溯 & 剪枝 & 去重思路)

题目描述

给定一个候选人编号的集合 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 res
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

深度不学习!!

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值