将字符串分割为回文子串

将字符串分割为回文子串

      • 题目描述
        • DFS解法代码
        • 动态规划代码
      • 题目描述
        • 求解代码

题目描述

给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。

返回 s 所有可能的分割方案。

示例:

输入: "aab"
输出:
[
  ["aa","b"],
  ["a","a","b"]
]

  题目要找到所有的回文子串组合方式,看到这个问题很容易就想到了暴力的处理方式,就是从每一个回文子串处深度遍历。因为深度遍历可以保证不重不漏。再说的具体一些,遍历的时候就是判断字符串的第i个字符到第j个字符是否是回文串,如果是,此时会产生分支,可以逐一字符的往前走,也可以把i到j的字符串当成一个整体往下走,这个过程很好写代码。
  而且python很方便判断字符串i到j是否是回文串,只需要s[i:j+1] == s[i:j+1][::-1]即可,如果其他语言没有这个特性,可以参考之前我上一篇文章,动态规划求解最长回文串中的回文串判断方式。深度优先遍历代码如下。

DFS解法代码

 def partition_dfs(s: str) -> List[List[str]]:
     res = []

     def dfs(start, tmp):
         if start == len(s):
             res.append(tmp)
         for i in range(start+1, len(s) + 1):
             if s[start:i] == s[start:i][::-1]:  # 如果start 到 i 是回文串,则进行深度遍历
                 dfs(i, tmp + [s[start:i]])
     dfs(0, [])
     return res

  递归的代码总是写起来很简洁,但是容易出现重复计算,我们思考如何把重复计算的开销给省下来,这必然要涉及到空间换时间。
  联想到回文子串的处理方式,使用动态规划比较容易的找到字符串的所有回文子串,一种直接的思路就是先把所有的回文子串求出来,使得可以在 O ( 1 ) O(1) O(1)的时间判断字符串s[i:j]是否是回文子串,然后根据这个判断结果去回溯,减少不必要的重复。这个代码只需要在上述过程加一个求是否是回文子串即可。求法同样可以参考上篇文章,动态规划求解最长回文串中的回文串判断方式。
  但是我们这里思考直接的自底向上的动态规划方式。可以直接思考后一状态和前一状态的关系,字符串s[:j]的所有分割方式就等于s[:i]的分割方式和s[i:j]的所有分割方式的全排列。如果s[i:j]中没有回文串,那么分割方式很单一,反之,才会对s[:j]的状态产生影响。
  使用 d p i dp_i dpi来表示到第i个字符为止,可以产生的所有分割, d p j = d p i ∗ s [ i : j ] i f   s [ i : j ] 是 回 文 串 dp_j=dp_i * s[i:j]\quad if\ s[i:j]是回文串 dpj=dpis[i:j]if s[i:j]。这里的 ∗ * 表示笛卡尔积,为了保持第一个字符的处理和后面的字符处理一致,可以在让 d p − 1 dp_-1 dp1等于一个列表,这样方便和后面的作加法。

动态规划代码

    def partition(s: str) -> List[List[str]]:
        dp = [[] for _ in range(len(s) + 1)]
        dp[-1] = [[]]
        for end in range(1,len(s)+1):
            for start in range(end):
                if s[start:end] == s[start:end][::-1]:
                    for each in dp[start-1]: # 这里就是做笛卡尔积的过程
                        dp[end-1].append(each+[s[start:end]])
        return dp[len(s)-1]

  这里需要注意的就是,如果使用动态规划,在处理字符串的时候,会导致下标因为平移的问题,下标处理起来不是很容易,推荐在写代码的时候从后往前计算,因为动态规划从两端哪一端开始,在这里是不影响结果的(大部分不影响)。
  完成了这个问题,其他类似的问题也就很好处理了。

题目描述

给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。

返回符合要求的最少分割次数。

示例:
输入: “aab”
输出: 1
解释: 进行一次分割就可将 s 分割成 [“aa”,“b”] 这样两个回文子串。

  表明上看这道题比上面的更简单,但是从思维方式上来看,上一道题更加符合思维方式一些,而这个问题要求的复杂度优化也就更高,上面的题目,可以让大家从暴力搜索到动态规划,这道题就很难直接想到动态规划了。但是有上面的题做基础,就比较容易。在这里就不扩展其他方法,直接基于已做的题目进行改代码。
  首先,我们已经能够在 O ( n 2 ) O(n^2) O(n2)的时间复杂度求出所有的回文串的起始位置和终止位置。同样回到上个问题,如果s[i:j]是一个回文串,那么显然 d p j dp_j dpj的最小分割次数,就等于 d p j = m i n ( d p i + 1 ) i f   s [ i : j ] 是 回 文 串 dp_j=min(dp_i + 1)\quad if \ s[i:j]是回文串 dpj=min(dpi+1)if s[i:j]。到这里代码就很好写了。可以采取一种思路就是先求出所有的回文串,然后使用递推式来求结果。
  这里介绍一种很取巧的方式,直接记录所有的回文串的起始和终止位置,然后对所有的位置使用递推式进行更改,这里要注意的就是更改的顺序需要按照结束的位置排序之后的顺序,否则 d p i dp_i dpi所求的还不是最终结果,这里就已经使用 d p i dp_i dpi计算 d p j dp_j dpj,就会产生错误。
  当然也可以在求是否是回文串的同时,直接求算出 d p j dp_j dpj。代码如下。

求解代码

def minCut(s: str) -> int:
    # 直接求以第j个字符结尾的字符串,并计算dp
    length = len(s)
    dp = list(range(-1,len(s)))
    pre = [True] + [False]*(length-1)
    for end in range(1,length):
        keys = [False if i!=end else True for i in range(length) ]
        # 记录当前以end结尾的,以i起始的位置是否是回文串
        dp[end+1] = dp[end] +1
        for beg in range(end):
            if s[beg] == s[end] and  (pre[beg+1] or beg+1==end):# 判断是否是回文串
                dp[end+1] = min(dp[beg]+1,dp[end+1])
                keys[beg] = True
        pre = keys # 保存以end-1为止第i个位置起始的字符串是否是回文串

    return dp[-1]

  动态规划问题重要的是培养动态规划的思维方式,如何定义问题,进而化简问题,动态规划的问题都是有特征的,对动态规划的直觉是可以培养的。
  如果真的没有直觉,建议多花点时间。可以从我这篇文章中学习,对一个问题就按部就班的从暴力,到递归再到动态规划,这也是一个重要的思维途径。

你可能感兴趣的:(算法,leetcode,python,算法,动态规划,回文串)