题目描述
给定一个字符串 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
递归的代码总是写起来很简洁,但是容易出现重复计算,我们思考如何把重复计算的开销给省下来,这必然要涉及到空间换时间。
联想到回文子串的处理方式,使用动态规划比较容易的找到字符串的所有回文子串,一种直接的思路就是先把所有的回文子串求出来,使得可以在
的时间判断字符串s[i:j]是否是回文子串,然后根据这个判断结果去回溯,减少不必要的重复。这个代码只需要在上述过程加一个求是否是回文子串即可。求法同样可以参考上篇文章,动态规划求解最长回文串中的回文串判断方式。
但是我们这里思考直接的自底向上的动态规划方式。可以直接思考后一状态和前一状态的关系,字符串s[:j]的所有分割方式就等于s[:i]的分割方式和s[i:j]的所有分割方式的全排列。如果s[i:j]中没有回文串,那么分割方式很单一,反之,才会对s[:j]的状态产生影响。
使用
来表示到第i个字符为止,可以产生的所有分割,
。这里的
表示笛卡尔积,为了保持第一个字符的处理和后面的字符处理一致,可以在让
等于一个列表,这样方便和后面的作加法。
动态规划代码
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”] 这样两个回文子串。
表明上看这道题比上面的更简单,但是从思维方式上来看,上一道题更加符合思维方式一些,而这个问题要求的复杂度优化也就更高,上面的题目,可以让大家从暴力搜索到动态规划,这道题就很难直接想到动态规划了。但是有上面的题做基础,就比较容易。在这里就不扩展其他方法,直接基于已做的题目进行改代码。
首先,我们已经能够在
的时间复杂度求出所有的回文串的起始位置和终止位置。同样回到上个问题,如果s[i:j]是一个回文串,那么显然
的最小分割次数,就等于
。到这里代码就很好写了。可以采取一种思路就是先求出所有的回文串,然后使用递推式来求结果。
这里介绍一种很取巧的方式,直接记录所有的回文串的起始和终止位置,然后对所有的位置使用递推式进行更改,这里要注意的就是更改的顺序需要按照结束的位置排序之后的顺序,否则
所求的还不是最终结果,这里就已经使用
计算
,就会产生错误。
当然也可以在求是否是回文串的同时,直接求算出
。代码如下。
求解代码
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]
动态规划问题重要的是培养动态规划的思维方式,如何定义问题,进而化简问题,动态规划的问题都是有特征的,对动态规划的直觉是可以培养的。
如果真的没有直觉,建议多花点时间。可以从我这篇文章中学习,对一个问题就按部就班的从暴力,到递归再到动态规划,这也是一个重要的思维途径。