给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是回文串。
返回符合要求的 最少分割次数 。
示例 1:
输入:s = "aab"
输出:1
解释:只需一次分割就可将 s 分割成 ["aa","b"] 这样两个回文子串。
示例 2:
输入:s = "a"
输出:0
示例 3:
输入:s = "ab"
输出:1
提示:
1 <= s.length <= 2000
s 仅由小写英文字母组成
动态规划
python
class Solution:
def minCut(self, s: str) -> int:
# dp[i] 表示从第i个字符构成的字符串可以划分成的最少子回文串,字符从0开始计数
# dp[i] = min(序列S) S中元素为 dp[k] + 1 k from 0 to i-1 且 s[k+1:i]为回文串
# 每种情况能否成立需要知道+1是否成立,也就是需要知道,右边剩余字符串是否是回文子串
# 那么如何求的s中任意连续字串是否为回文串呢?
n = len(s)
judge = [[False] * n for _ in range(n)]
for i in range(n - 1):
g, h = i, i + 1
while -1 < g < h < n and s[g] == s[h]:
judge[g][h] = True
g -= 1
h += 1
for i in range(n):
judge[i][i] = True
g, h = i - 1, i + 1
while -1 < g < h < n and s[g] == s[h]:
judge[g][h] = True
g -= 1
h += 1
dp = [i for i in range(1, n + 1)]
for i in range(n):
if judge[0][i]:
dp[i] = 1
for i in range(1, n):
for j in range(i):
if judge[j + 1][i]:
dp[i] = min(dp[i], dp[j] + 1)
return dp[n - 1] - 1
时间复杂度: O ( n 2 ) O(n^2) O(n2)。计算字符串中所有回文串耗时 O ( n ) O(n) O(n)。动态规划计算最少划分次数外层循环次数函数为 l a y e r 1 ( n ) = n layer1(n)=n layer1(n)=n,求面积得耗时 O ( n ∗ n / 2 ) = O ( n 2 ) O(n*n/2)=O(n^2) O(n∗n/2)=O(n2)。
空间复杂度:字符串回文串判定消耗 O ( n 2 ) O(n^2) O(n2),动态规划dp数组消耗 O ( n ) O(n) O(n),空间复杂度 O ( n 2 ) O(n^2) O(n2)。
本题动态规划的思路相对好想,但是关于字符串中所有回文串的判定处理不好会导致时间复杂度过高,导致超时,预处理序列是一种常用的以空间换时间的方法。下面总结一下本题以及一些拓展:
在 O ( n 2 ) O(n^2) O(n2)耗时求出字符串中所有连续字符子串是否为回文串。
设dp[i][j]表示从第i个字符到第j个字符构成的字串是否为回文串,字符从0开始计数
最经典的分成两种情况讨论。
若回文串为偶数长度,则最中间两个数往外扩展一定对称,所以长度为n的字符串中连续两个数会有n-1对,对每一对数据进行判断即可
若回文串为奇数长度,则选一个中间数字往两边扩展即可,共有n中选法。
python
n = len(s)
judge = [[False] * n for _ in range(n)]
for i in range(n - 1):
g, h = i, i + 1
while -1 < g < h < n and s[g] == s[h]:
judge[g][h] = True
g -= 1
h += 1
for i in range(n):
judge[i][i] = True
g, h = i - 1, i + 1
while -1 < g < h < n and s[g] == s[h]:
judge[g][h] = True
g -= 1
h += 1
而事实上,这里有一种很巧妙地方式可以合并上面两种情况。
奇数和偶数的区别在于,有没有中间值,如果只有一个字符,那么一定是回文串,假设不考虑中间到底有没有字符,那么一个回文串最外面的左右字符一定得相同。假设左边是第i个字符,右边是第j个字符,那么dp[i] == dp[j],若i到j构成回文串那么i+1到j-1也一定构成,则有
dp[i][j] = dp[i] == dp[j] and dp[i+1][j-1]
不过这里有一些特殊情况:
- 当i==j时,dp[i+1][j-1]中i+1>j-1导致用到了一个无意义的状态
- 当i+1==j时,同理,也用到了一个无意义状态
不过巧合的是,当用到无意义状态的时候,只需要看dp[i] == dp[j] 即可,所以这里等效可以把无意义状态全部置为True。
则会有完整状态转移方程。
python
# 由于在计算dp[i][j]的时候不会用到dp[i][j]本身的值,所以可以同过全部赋值True的方式简化赋初值
# 只需要计算矩阵的一半即可,对角线也无需计算
n = len(s)
judge = [[True] * n for _ in range(n)]
for i in range(n - 1):
for j in range(i + , n):
judge[i][j] = judge[i + 1][j - 1] and s[i] == s[j]
另外一个场景
给定数字序列,从第i个数字到第j个数字之和,求出所有情况的和。
蛮力就是三层循环,第一层i,第二层j,第三层枚举求和
但实际上可以优化,求和即可,dp[i]为第0到第i个数字的和那么f[i][j] = dp[j] - dp[i-1]
这样只需要O(n)时间就可以构造好f[i][j]的结果,O(n)通常不会影响到核心解题逻辑的时间复杂度。
还有类似求最大值,给定数组序列,f[i]为除第i个数之外所有数的最大值,最小值也同理。只需遍历一次找到最大值和次大值,第i个数不是最大值f[i]就是最大值,第i个数是最大值f[i]就是次大值。
又如前后缀数组等等。