回溯实战篇2

前言

今天继续带大家进行回溯的实战篇2,去学习如何用回溯的方法去解决分割和子集的问题,最重要的就是学会回溯三部曲的构建,一文带大家弄懂。本文用于记录自己的学习过程,同时向大家进行分享相关的内容。本文内容参考于代码随想录同时包含了自己的许多学习思考过程,如果有错误的地方欢迎批评指正!

image-20250509235248135

分割

分割回文串

131. 分割回文串 - 力扣(LeetCode)

image-20250509235347673

相关技巧:其实这种分割的问题和组合问题是类似的,我们来看如果是组合问题:那是不是就是选取一个a之后,在bcdef中再去选取第二个,然后选取b之后在cdef中再选取第三个。切割问题问题也是同样的道理:切割一个a之后,在bcdef中再去切割第二段,切割b之后在cdef中再切割第三段。我们再来看这道分割的题目,其实就是两个步骤,我们先把分割的结果找出来,然后再判断其每段分割是不是回文串。这里有个小技巧,就是我们可以再进入回溯前进行判断分割的该段是不是回文,是回文那就进入回溯去继续分割,不是回文就返回。

image-20250510084430773

我们来通过一个成功的结果来思考我们该如何去通过回溯方法来写代码。首先切割的范围是[a,a,b],切割出第一个a之后,我们判断[a]是不是回文串,一个字母当然是了,所以继续切割a,就得到了[a],[a],第二个a同样的也是回文串,继续切割,得到[b],同样的也是回文串,这时候切割到最后了,都是回文串那么就保存路径结果退出单层回溯。然后这时候就回溯到了[a]和[a,b]的时候,这时候我们再[a,b]后进行切割,判断[a,b]不属于回文串,直接退出单层回溯。所以这样再来看我们的逻辑就跟清晰了,回溯三部曲就能够很好的写出来了。

  • 确定回溯的参数和返回值:字符串s,我们用来记录切割到哪里的startindex参数,路径参数path,用来保存路径进行回溯操作的,最后就是保存结果的result参数了。返回值不需要了,有result来保存结果了。
  • 确定回溯的终止条件:当Startindex的大小等于字符串的长度的时候,说明切割到最后了,直接加入结果退出回溯。
  • 确定单层回溯的逻辑:首先判断是否切割到最后,没有就继续当前的回溯过程,从当前的位置开始往后切割,切割完之后,判读当前的子串是否是回文串,回文串就进入回溯继续切割,不是回文串就往后再切割直到是回文串,或者切割到最后为止。
class Solution:

    def partition(self, s: str) -> List[List[str]]:
        '''
        递归用于纵向遍历
        for循环用于横向遍历
        当切割线迭代至字符串末尾,说明找到一种方法
        类似组合问题,为了不重复切割同一位置,需要start_index来做标记下一轮递归的起始位置(切割线)
        '''
        result = []
        self.backtracking(s, 0, [], result)
        return result

    def backtracking(self, s, start_index, path, result ):
        # Base Case
        if start_index == len(s):
            result.append(path[:])
            return
        
        # 单层递归逻辑
        for i in range(start_index, len(s)):
            # 此次比其他组合题目多了一步判断:
            # 判断被截取的这一段子串([start_index, i])是否为回文串
            if self.is_palindrome(s, start_index, i):
                path.append(s[start_index:i+1])
                self.backtracking(s, i+1, path, result)   # 递归纵向遍历:从下一处进行切割,判断其余是否仍为回文串
                path.pop()             # 回溯


    def is_palindrome(self, s: str, start: int, end: int) -> bool:
        i: int = start        
        j: int = end
        while i < j:
            if s[i] != s[j]:
                return False
            i += 1
            j -= 1
        return True

复原IP地址

93. 复原 IP 地址 - 力扣(LeetCode)

image-20250509235502535

相关技巧:这道题看着好像比较复杂,但是其逻辑跟分割回文串是一样的道理了,我们分割回文串的时候,判断分割的串是不是回文串,那么这里呢,我们分割完之后,就是判断当前的分割区间是否合法。只不过把回文串的判断换成了是否合法的判断了。那么不合法的情况有哪些呢?就是0开头的数字不合法、遇到非数字字符不合法、如果大于255了不合法。并且其终止条件也不像分割回文串分割到最后了,而是分割了三次就是结束了。

image-20250510090158161

其实整个过程跟分割回文串差不多,相信大家看图就能够很好的理解了。

  • 确定回溯的参数和返回值:其实与分割回文串的参数差不多,字符串s,我们用来记录切割到哪里的startindex参数,路径参数current,用来保存路径进行回溯操作的,还有个pointnum用来记录我们分割了几次,最后就是保存结果的result参数了。返回值不需要了,有result来保存结果了。
  • 确定回溯的终止条件:当pointnum的大小等于3的时候,说明切割成四段了,再判断最后的部分是否合法,合法就直接加入结果退出回溯。
  • 确定单层回溯的逻辑:首先判断是否切割三次了,没有就继续当前的回溯过程,从当前的位置开始往后切割,切割完之后,判读当前的子串是否是合法,合法就进入回溯继续切割,不合法就往后再切割直到合法为止,或者切割到最后为止。
class Solution:
    def restoreIpAddresses(self, s: str) -> List[str]:
        result = []
        self.backtracking(s, 0, 0, "", result)
        return result

    def backtracking(self, s, start_index, point_num, current, result):
        if point_num == 3:  # 逗点数量为3时,分隔结束
            if self.is_valid(s, start_index, len(s) - 1):  # 判断第四段子字符串是否合法
                current += s[start_index:]  # 添加最后一段子字符串
                result.append(current)
            return

        for i in range(start_index, len(s)):
            if self.is_valid(s, start_index, i):  # 判断 [start_index, i] 这个区间的子串是否合法
                sub = s[start_index:i + 1]
                self.backtracking(s, i + 1, point_num + 1, current + sub + '.', result)
            else:
                break

    def is_valid(self, s, start, end):
        if start > end:
            return False
        if s[start] == '0' and start != end:  # 0开头的数字不合法
            return False
        num = 0
        for i in range(start, end + 1):
            if not s[i].isdigit():  # 遇到非数字字符不合法
                return False
            num = num * 10 + int(s[i])
            if num > 255:  # 如果大于255了不合法
                return False
        return True

子集

子集

78. 子集 - 力扣(LeetCode)

image-20250509235535318

相关技巧:来看子集问题,如果将组合,分割,子集这些问题都当作树结构来看,组合和分割的最终结果是不是就是找到叶子节点,但是子集问题不同,子集问题是为了找到树种所有的节点。其实也还是跟组合问题一样的,组合问题到最终的叶子节点就保存结果到结果集,而子集问题就是再每个单层回溯的时候都保留当前的结果到结果集。

image-20250510094511373

实现路径跟组合一样,唯一不同的就是需要保存每个中间结果。

  • 确定回溯的参数和返回值:我们来看回溯参数,和组合问题是一样的道理。nums肯定是需要的,然后我们还需要个srartindex用来记录当前从哪里开始,并且在每下个回溯的时候,需要在当前的基础上加1,这样我们就避免了重复的可能。还有我们需要有一个参数path来存储我们的路径,用来加入新的和弹出操作,最后当结果符合,我们肯定需要一个result来保存我们的结果。这就是我们该问题中所需要的参数了。至于返回值就不需要了,因为我们有result参数来保存我们的结果了。
  • 确定回溯的终止条件:就是当startindex大于当前的nums长度就终止,超过长度就没意义了。
  • 确定单层回溯的逻辑:首先第一步就是将当前的结果加入我们的结果集,因为我们是找所有的节点,也就是所有的结果包括中间结果,然后就是判断是否超出长度了,然后就是正常回溯的操作了(跟组合的一样)。
class Solution:
    def subsets(self, nums):
        result = []
        path = []
        self.backtracking(nums, 0, path, result)
        return result

    def backtracking(self, nums, startIndex, path, result):
        result.append(path[:])  # 收集子集,要放在终止添加的上面,否则会漏掉自己
        if startIndex >= len(nums):  
            return
        for i in range(startIndex, len(nums)):
            path.append(nums[i])
            self.backtracking(nums, i + 1, path, result)
            path.pop()

子集II

90. 子集 II - 力扣(LeetCode)

image-20250509235611787

相关技巧:这道题,其实就是类似于子集和组合总和II问题的结合。我们不仅需要每个中间过程的值,并且其有重复的,我们还需要去重,还记得我们之前怎么去重的吗?是的这个时候used数组又需要登场了。

image-20250510101405711

具体的实现过程就是在子集的基础上加了个去重,也就是used数组的使用,这个就不在叙述了,可以去看看回溯实战1的组合总和II讲述怎么使用的。

  • 确定回溯的参数和返回值:我们来看回溯参数,和子集问题是一样的道理,不过就是多了个used数组用来保证我们的去重。其他的就是nums肯定是需要的,然后我们还需要个srartindex用来记录当前从哪里开始,并且在每下个回溯的时候,需要在当前的基础上加1,这样我们就避免了重复的可能。还有我们需要有一个参数path来存储我们的路径,用来加入新的和弹出操作,最后当结果符合,我们肯定需要一个result来保存我们的结果。这就是我们该问题中所需要的参数了。至于返回值就不需要了,因为我们有result参数来保存我们的结果了。
  • 确定回溯的终止条件:就是当startindex大于当前的nums长度就终止,超过长度就没意义了。
  • 确定单层回溯的逻辑:首先第一步就是将当前的结果加入我们的结果集,因为我们是找所有的节点,也就是所有的结果包括中间结果,然后就是判断是否超出长度了,然后就是循环中,我们需要有个判断,使用used数组保证同个树枝中可以使用相同的,同个数层中不能使用相同的数值。
class Solution:
    def subsetsWithDup(self, nums):
        result = []
        path = []
        used = [False] * len(nums)
        nums.sort()  # 去重需要排序
        self.backtracking(nums, 0, used, path, result)
        return result

    def backtracking(self, nums, startIndex, used, path, result):
        result.append(path[:])  # 收集子集
        for i in range(startIndex, len(nums)):
            # used[i - 1] == True,说明同一树枝 nums[i - 1] 使用过
            # used[i - 1] == False,说明同一树层 nums[i - 1] 使用过
            # 而我们要对同一树层使用过的元素进行跳过
            if i > 0 and nums[i] == nums[i - 1] and not used[i - 1]:
                continue
            path.append(nums[i])
            used[i] = True
            self.backtracking(nums, i + 1, used, path, result)
            used[i] = False
            path.pop()
posted @ 2025-05-14 11:13  carpell  阅读(21)  评论(0)    收藏  举报