代码随想录算法训练营Day27 | 39. 组合总和 | 40. 组合总和II | 131. 分割回文串

39. 组合总和

题目链接 | 解题思路

本题明显是一个集合内求组合,所以需要 start_idx 来记录递归的位置。
注意的是本题允许重复的元素,终止条件也是当前记录的子集和,所以对于递归的 start_idx 不需要 +1。

优化剪枝:在 for loop 中如果当前子集和大于 target 的话直接结束循环。这是一个通用的剪枝手段。

class Solution:
    def __init__(self):
        self.curr_record = []
        self.results = []

    def backtrack(self, candidates: List[int], target: int, start: int):
        if sum(self.curr_record) >= target:
            if sum(self.curr_record) == target:
                print(start)
                self.results.append(self.curr_record.copy())
            return None
        
        for i in range(start, len(candidates)):
            self.curr_record.append(candidates[i])
            self.backtrack(candidates, target, i)
            self.curr_record.pop()

    def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
        self.backtrack(candidates, target, 0)
        return self.results   

40. 组合总和II

题目链接 | 解题思路

这道题和之前所有回溯题最大的不同:可选集合中存在重复元素。这导致在答案中进行去重变得比之前复杂很多,因为依靠 start_idx 的单调性无法有效去重+获得正确答案了。例如 candidates = [10, 1, 2, 7, 6, 1, 5], target = 8,答案中的 [1, 1, 6] 成立是因为有两个元素值均为1,不是重复使用;但是 [1, 7][7, 1] 就是重复答案了。

按照简单思路得到所有的解之后,以 set 的形式去重,这样很容易导致超时。正确的做法是在遍历中进行去重,即使用过的元素就不要再使用了

  • 路径去重:对于一条 path 来说,父节点用过的元素(元素,而非元素的值)显然不能再使用
    • 这一点之前的题都有所体现,即 start_idx + 递归+回溯
  • 层级去重:同一层内,左侧节点已经使用过的元素值应该被直接跳过(去重)
    • 例如:candidates = [1, 1, 2], target = 3。如果只依靠 start_idx 进行去重,在访问第一个 1 的时候会得到 [1, 2],在访问第二个 1 的时候同样会得到 [1, 2],会得到重复的子集。解决方法是,遇到第二个 1 的时候应该直接跳过。
    • 核心思想:遇到一个新的元素的时候,当前的回溯+遍历已经可以得到所有包含该元素值的解,之后遇到的具有相同值的元素只能给出之前解的子集。因此,针对每个元素值,只需要第一次遇到时进行回溯+递归即可,重复值的元素直接跳过。

以上的思路需要对 candidates 提前排序。

class Solution:
    def __init__(self):
        self.curr_record = []
        self.results = []
    
    def backtrack(self, candidates: List[int], target: int, start: int):
        if sum(self.curr_record) >= target:
            if sum(self.curr_record) == target:
                self.results.append(self.curr_record.copy())
            return None

        curr_idx = start
        while (curr_idx < len(candidates)):
            curr_candidate = candidates[curr_idx]
            self.curr_record.append(curr_candidate)
            self.backtrack(candidates, target, curr_idx + 1)
            self.curr_record.pop()

            curr_idx += 1
            while (curr_idx < len(candidates) and candidates[curr_idx] == curr_candidate):
                curr_idx += 1

    def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
        candidates.sort()
        self.backtrack(candidates, target, 0)
        return self.results

另外的方法:不要使用已使用过的元素,显然可以额外维持一个数组 used 来记录用过的元素
这种方法通过额外数组来分辨当前发生的元素值重复是由于

  • 路径内重复:两个重复元素的前一个 used=True,代表着当前路径内使用过前一个元素,则重复元素的后一个还是可以使用
  • 层级内重复:两个重复元素的前一个 used=Flase,代表着前一个元素在同一层之前被用过了,则重复元素的后一个不能使用(会导致重复的子集解)

在这里插入图片描述

这个方法的思路有点复杂,很接近以前在二叉树删除节点中保持双指针记录父节点的方式。

131. 分割回文串

题目链接 | 解题思路

本题可以拆成两段:

  • 回文串判断:简单的递归/双指针/ reverse 就能解决,注意不要忽略单一字母的情况
  • 分割字符串:和组合类问题其实很相似,都是通过传递 index 的方式遍历所有的组合。不同之处在于,连续子字符串要求“分割的组合”具有连续性,所以单层搜索中要以结尾切割线的形式利用传入的 index。

在这里插入图片描述

回溯的组件:

  • 参数和返回值:两个经典的全局变量;一个额外的 start_idx 传递起始点
  • 终止条件:start_idx = len(s),代表着分割线已经到了整个字符串的尾端,当前的分割路径已经结束了
  • 单层搜索的逻辑:注意分割线的语义,这里我使用的是 [,] 左闭右闭的写法。

本题的难点在于分析出和组合的相似之处,从而能够以组合的思路解决字符串分割。同时,理解语义并且写出正确的单层搜索逻辑也很关键。

class Solution:
    def __init__(self):
        self.curr_record = []
        self.results = []
    
    def palindrome(self, s: str) -> bool:
        if len(s) <= 1:
            return True

        if s[0] != s[-1]:
            return False
        
        return self.palindrome(s[1:-1])

    def backtrack(self, s: str, start_idx: int):
        if start_idx == len(s):
            self.results.append(self.curr_record.copy())
            return None
        
        for curr_idx in range(start_idx, len(s)):
            curr_s = s[start_idx: curr_idx + 1]         # [,)
            if self.palindrome(curr_s):                  # if not palindrome, skip the iteration
                self.curr_record.append(curr_s)
                self.backtrack(s, curr_idx + 1)
                self.curr_record.pop()

    def partition(self, s: str) -> List[List[str]]:
        self.backtrack(s, 0)
        return self.results

可以用 dp 优化判断回文串的方法,但不是重点,

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值