Bootstrap

【回溯法】——分割回文串

131. 分割回文串

一、题目难度

中等

二、相关标签与相关企业

[相关标签]
[相关企业]

三、题目描述

给你一个字符串 s s s,请你将 s s s 分割成一些子串,使每个子串都是回文串。返回 s s s 所有可能的分割方案。

四、示例

示例1

输入 s = s = s= “aab”
输出[["a","a","b"],["aa","b"]]

示例2

输入 s = s = s= “a”
输出[["a"]]

五、提示

1 ≤ s . l e n g t h ≤ 16 1 \leq s.length \leq 16 1s.length16
s s s 仅由小写英文字母组成

六、解法解读

(一)第一种解法(明月高楼休独倚的解法)

class Solution:
    def partition(self, s: str) -> List[List[str]]:
        res = []
        def backtrack(List, tmp):
            """
            回溯函数,用于尝试不同的分割方式,找到所有满足条件的分割方案

            :param List: 剩余待分割的字符串
            :param tmp: 当前已经分割出来的回文子串列表
            """
            if not List:
                res.append(tmp)
                return

            for i in range(len(List)):
                if List[:i + 1] == List[i::-1]:
                    backtrack(List[i + 1:], tmp + [List[:i + 1]])

        backtrack(s, [])
        return res

思路解读

  • 整体思路是通过回溯算法来尝试所有可能的字符串分割方式,找出使得每个分割出来的子串都是回文串的所有方案。
  • 定义结果列表和回溯函数
    • 首先创建了一个空列表 res,用于存储最终所有满足条件的分割方案。
    • 然后定义了内部函数 backtrack,它接受两个参数:List 表示剩余待分割的字符串,tmp 表示当前已经分割出来的回文子串列表。
  • 回溯函数的终止条件
    • List 为空字符串时,意味着已经完成了对原始字符串 s 的一种分割方式,此时将当前的分割结果 tmp 添加到 res 中,然后返回,结束当前层的回溯。
  • 回溯搜索的遍历过程
    • 通过循环遍历剩余待分割字符串 List 的每个可能的分割位置 i(从 0len(List) - 1)。
    • 对于每个分割位置 i,判断从字符串开头到 i + 1 位置的子串(即 List[:i + 1])是否为回文串。判断方法是比较该子串与其反转后的字符串(即 List[i::-1])是否相等。如果是回文串,就进行下一步操作。
    • 当确定 List[:i + 1] 是回文串后,就以这个回文串作为当前分割出来的一部分,继续对剩余的字符串(即 List[i + 1:])进行回溯分割。这里通过递归调用 backtrack 函数,同时更新参数:将剩余待分割字符串更新为 List[i + 1:],将当前已经分割出来的回文子串列表更新为 tmp + [List[:i + 1]],也就是把新找到的回文串添加到 tmp 列表中。

(二)第二种解法(喳嗑睿姜的解法)

class Solution:
    def partition(self, s: str) -> List[List[str]]:
        res, ans = [], []
        def backtrack(s):
            """
            回溯函数,用于尝试不同的分割方式,找到所有满足条件的分割方案

            :param s: 剩余待分割的字符串
            """
            if len(s) == 0:
                res.append(ans)
                return

            for i in range(len(s)):
                if s[:i + 1] == s[:i + 1][::-1]:
                    ans.append(s[:i + 1])
                    backtrack(s[i + 1:])
                    ans.pop()
        backtrack(s)
        return res

思路解读

  • 同样是基于回溯算法来解决问题,整体思路和第一种解法类似,但在具体实现上有一些细节差异。
  • 定义结果列表和辅助列表以及回溯函数
    • 创建了两个空列表 resans,其中 res 用于存储最终所有满足条件的分割方案,ans 则在回溯过程中用于临时存储当前正在构建的一种分割方案中的回文子串。
    • 定义了内部函数 backtrack,它只接受一个参数 s,表示剩余待分割的字符串。
  • 回溯函数的终止条件
    • 当剩余待分割字符串 s 的长度为 0 时,意味着已经完成了对原始字符串的一种分割方式,此时将当前临时存储分割方案的 ans 列表添加到 res 中,然后返回,结束当前层的回溯。
  • 回溯搜索的遍历过程
    • 通过循环遍历剩余待分割字符串 s 的每个可能的分割位置 i(从 0len(s) - 1)。
    • 对于每个分割位置 i,判断从字符串开头到 i + 1 位置的子串(即 s[:i + 1])是否为回文串。判断方法是比较该子串与其反转后的字符串(即 s[:i + 1][::-1])是否相等。如果是回文串,就进行下一步操作。
    • 当确定 s[:i + 1] 是回文串后,将这个回文串添加到 ans 列表中,表示当前找到了一个回文子串作为分割的一部分。然后对剩余的字符串(即 s[i + 1:])进行回溯分割,通过递归调用 backtrack 函数,传入剩余待分割字符串 s[i + 1:]
    • 在完成对剩余字符串的递归回溯后(无论是否找到了完整的分割方案),需要将刚才添加到 ans 列表中的回文串弹出(即 ans.pop()),恢复到添加该回文串之前的状态,以便能够尝试下一个可能的分割位置,继续构建其他可能的分割方案。

(三)第二种解法存在的问题及深拷贝、浅拷贝相关解释

存在的问题
在第二种解法中,当执行到 res.append(ans) 这一步时,出现了问题。这里并不是深拷贝和浅拷贝的问题(虽然确实和对象引用有关),而是因为 ans 是一个列表,在Python中,列表是可变对象。当把 ans 添加到 res 中时,实际上添加的是 ans 的引用,而不是 ans 的一个独立副本。

这就意味着,后续在回溯过程中,如果对 ans 进行了修改(比如通过 ans.pop() 弹出元素),那么已经添加到 res 中的那个引用所指向的对象也会被修改。所以最终得到的 res 列表中的所有元素(原本应该是不同的分割方案)实际上都指向了同一个 ans 对象,并且这个对象在不断被修改,导致最终 res 中的结果是不正确的,可能会出现空值或者不符合预期的情况。

深拷贝和浅拷贝相关解释

  • 浅拷贝:在Python中,当对一个对象进行浅拷贝时,会创建一个新的对象,但是这个新对象中的元素(如果元素是可变对象)仍然指向原来对象中的元素的引用。例如,对于一个列表 a = [1, 2, [3, 4]],如果对它进行浅拷贝 b = a.copy(),那么 b 是一个新的列表对象,但是 b 中的子列表 [3, 4] 仍然和 a 中的 [3, 4] 指向同一个对象。所以如果修改了 a 中的子列表 [3, 4],那么 b 中的 [3, 4] 也会被修改。
  • 深拷贝:与浅拷贝不同,深拷贝会创建一个全新的对象,并且这个新对象中的所有元素(包括可变对象元素)都会被递归地拷贝,创建出完全独立的副本。例如,对于上面的列表 a,如果使用 c = deepcopy(a)(假设已经导入了 copy 模块中的 deepcopy 函数),那么 c 是一个全新的列表对象,其中的所有元素包括子列表 [3, 4] 都会有自己独立的副本,修改 a 中的任何元素都不会影响到 c

在第二种解法中,如果要解决这个问题,正确的做法应该是在 res.append(ans) 这一步,对 ans 进行深拷贝,将 ans 的一个独立副本添加到 res 中,这样后续对 ans 的修改就不会影响到已经添加到 res 中的分割方案。例如,可以修改为 res.append(ans.copy())(这里的 copy 方法是浅拷贝,对于简单的只包含不可变对象的列表可能足够了,但对于可能包含可变对象的列表,最好还是使用 deepcopy)或者导入 copy 模块并使用 res.append(copy.deepcopy(ans))

所以,第二种解法的问题在于没有正确处理列表对象的引用关系,导致结果不正确,而不是简单的深拷贝和浅拷贝问题,但确实涉及到了类似的对象引用和拷贝的概念。

class Solution:
    def partition(self, s: str) -> List[List[str]]:
        # res记录结果
        res = []     # res记录所有分割方案,每个方案用 tmp = [] 保存
        tmp = []     # 记录一种方案
        def backtrack(List,tmp):
        #"""
       # 回溯函数,用于尝试不同的分割方案,找到所有满足条件的分割方案
#
 #       :param List: 剩余待分割的字符串 
  #      :param tmp: 当前已经分割出来的回文子串列表,每个列表记录一种分割方案
   #     """
            if len(List) == 0:            # 没有剩余待分割字符串,全分完了
                res.append(tmp)     # 完成了一种分割方案,保存到res
                return              # 回溯到上一步
        
            for i in range(len(List)):   # 检查剩余的List是否存在回文情况
                if List[:i + 1] == List[i::-1]:  # List[:i+1]是0到i,List[i::-1]是i到0,每个对应元素都相等就说明0到i是回文串
                # 每找到一次回文串都相当于走到了下一个路口,到下个路口就调用自身递归
                # List是剩余待分割的字符串,更新为List[i + 1:]
                # tmp是记录当前的方案,更新为tmp + List[:i + 1]
                # backtrack(List[i + 1:],[tmp + List[:i + 1]])也错!
                # 错错错!应该是把List[:i + 1]作为一个单独的字符元素传入!

                    backtrack(List[i + 1:],tmp + [List[:i + 1]])
        
        # 
        backtrack(s, [])
        return res  

以下是这三种传入 backtrack 函数方式的区别:

tmp + [List[:i + 1]]

  • 这种方式首先使用切片操作 List[:i + 1] 取出从字符串 List 开头到第 i + 1 个位置的子串,然后将这个子串作为一个单独的元素放入一个新的列表 [List[:i + 1]] 中。接着,再将这个新列表与 tmp 列表进行拼接,得到一个新的列表作为参数传递给 backtrack 函数。
  • 例如,如果 tmp = ['a']List[:i + 1] = 'bc',那么 tmp + [List[:i + 1]] 的结果是 ['a', 'bc']。这符合我们的需求,即将当前找到的回文子串作为一个独立的元素添加到当前已有的分割结果列表 tmp 中,然后传递给下一层递归,用于构建完整的分割方案。

[tmp + List[:i + 1]]

  • 这里先将 tmpList[:i + 1] 进行拼接,得到一个新的列表,然后再将这个新列表作为唯一的元素放入另一个新的列表中。也就是说,最终传递给 backtrack 函数的是一个包含一个元素的列表,而这个元素又是一个列表。
  • 例如,同样 tmp = ['a']List[:i + 1] = 'bc',那么 [tmp + List[:i + 1]] 的结果是 [['a', 'bc']]。这种方式与我们期望的分割方案的构建方式不符,它会导致在后续的处理中,程序可能无法正确地识别和处理每个单独的回文子串,因为每个分割方案都被嵌套在了一个额外的列表中。

tmp + List[:i + 1]

  • 此方式直接将 List[:i + 1] 中的每个字符依次添加到 tmp 列表的末尾,实现了两个列表的拼接,但没有将 List[:i + 1] 当作一个整体的子串来处理。
  • 例如,当 tmp = ['a']List[:i + 1] = 'bc' 时,tmp + List[:i + 1] 的结果是 ['a', 'b', 'c']。这样就破坏了我们原本想要将每个完整的回文子串作为一个独立元素添加到分割结果列表中的意图,导致传递给 backtrack 函数的参数不符合正确的分割方案格式,进而影响程序对分割方案的正确构建和处理。

综上所述,只有 tmp + [List[:i + 1]] 这种方式能够正确地将当前找到的回文子串作为一个独立元素添加到当前的分割结果列表 tmp 中,并传递给 backtrack 函数,用于构建和探索所有可能的正确分割方案。其他两种方式都会导致传递给 backtrack 函数的参数不符合要求,从而无法得到正确的结果。

;