思路整理自公众号: labuladong
给定一个字符串 s ,找到其中最长的回文子序列,并返回该序列的长度。可以假设 s 的最大长度为 1000 。
示例 1:
输入:
"bbbab"
输出:
4
一个可能的最长回文子序列为 "bbbb"。
示例 2:
输入:
"cbbd"
输出:
2
一个可能的最长回文子序列为 "bb"。
提示:
1 <= s.length <= 1000
s 只包含小写英文字母
解题思路
注意子序列和子串的区别,子串是连续的,子序列是不连续的。
这类问题都是让你求一个最长子序列,因为最短子序列就是一个字符嘛,没啥可问的。一旦涉及到子序列和最值,那几乎可以肯定,考察的是动态规划技巧,时间复杂度一般都是 O(n^2)。
有两种思路定义dp数组。
1、第一种思路模板是一个一维的 dp 数组:
int n = array.length;
int[] dp = new int[n];
for (int i = 1; i < n; i++) {
for (int j = 0; j < i; j++) {
dp[i] = 最值(dp[i], dp[j] + ...)
}
}
2、第二种思路模板是一个二维的 dp 数组:
int n = arr.length;
int[][] dp = new dp[n][n];
for (int i = 0; i < n; i++) {
for (int j = 1; j < n; j++) {
if (arr[i] == arr[j])
dp[i][j] = dp[i][j] + ...
else
dp[i][j] = 最值(...)
}
}
搞清楚dp数组的含义
2.1 涉及两个字符串/数组时(比如最长公共子序列),dp 数组的含义如下:
在子数组arr1[0…i]和子数组arr2[0…j]中,我们要求的子序列(最长公共子序列)长度为dp[i][j]。
2.2 只涉及一个字符串/数组时(比如本文要讲的最长回文子序列),dp 数组的含义如下:
在子数组array[i…j]中,我们要求的子序列(最长回文子序列)的长度为dp[i][j]。
在本题中,定义dp数组,其含义为在子串s[i…j]中,最长回文子序列的长度为dp[i][j]
class Solution:
def longestPalindromeSubseq(self, s: str) -> int:
# 动态规划
n = len(s)
# dp[i][j] 表示在子串s[i:j+1]中,最长回文子序列的长度为dp[i][j]
dp = [[0 for _ in range(n)] for _ in range(n)]
# 他的子问题就是dp[i+1][j-1]
# 假设已经知道s[i+1:j]的最长子序列为dp[i+1][j-1],要求dp[i][j],
# 情况一:当s[i]==s[j]时,那么s[i:j+1]的最长回文子序列长度就是s[i+1:j]的最长子序列加上s[i]和s[j]
# 情况二:当s[i]!=s[j]时,说明它俩不可能同时出现在s[i:j+1]的最长回文子序列中,
# 那么把它俩分别加入s[i+1:j-1]中,看看哪个子串产生的回文子序列更长即可: dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
# if (s[i] == s[j])
# 它俩一定在最长回文子序列中
# dp[i][j] = dp[i + 1][j - 1] + 2;
# else
# s[i+1..j] 和 s[i..j-1] 谁的回文子序列更长?选择更长的子序列
# dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
# 初始化dp
for i in range(n):
dp[i][i] = 1
# 必须满足 i < j
for i in range(n - 1, -1, -1):
for j in range(i + 1, n):
if s[i] == s[j]:
dp[i][j] = dp[i + 1][j - 1] + 2
else:
dp[i][j] = max(dp[i][j - 1], dp[i + 1][j])
return dp[0][n - 1]
################################# 手动分割 ######################################
1312. 让字符串成为回文串的最少插入次数
给你一个字符串 s ,每一次操作你都可以在字符串的任意位置插入任意字符。
请你返回让 s 成为回文串的 最少操作次数 。
「回文串」是正读和反读都相同的字符串。
示例 1:
输入:s = "zzazz"
输出:0
解释:字符串 "zzazz" 已经是回文串了,所以不需要做任何插入操作。
示例 2:
输入:s = "mbadm"
输出:2
解释:字符串可变为 "mbdadbm" 或者 "mdbabdm" 。
示例 3:
输入:s = "leetcode"
输出:5
解释:插入 5 个字符后字符串变为 "leetcodocteel" 。
示例 4:
输入:s = "g"
输出:0
示例 5:
输入:s = "no"
输出:1
提示:
1 <= s.length <= 500
s 中所有字符都是小写字母。
解题思路
定义dp数组,dp[i][j]表示对于字符串s[i…j],(包含第j个),最少需要进行dp[i][j]次插入才可以变为回文字符串,最终的答案为dp[0][n-1]。
对于dp[i][j],dp[i+1][j-1]就是他的子问题。
class Solution(object):
def minInsertions(self, s):
"""
:type s: str
:rtype: int
"""
n = len(s)
# dp[i][j]表示对于字符串s[i...j],(包含第j个),最少需要进行dp[i][j]次插入才可以变为回文字符串,最终的答案为dp[0][n-1]
# 当i==j时,相当于是一个字符,本身就是回文,所以dp[i][i]==0
dp = [[0 for _ in range(n)] for _ in range(n)]
# 状态转移方程
# 对于dp[i][j],dp[i+1][j-1]就是他的子问题,假设已经计算出dp[i+1][j-1],相当于s[i:j+1]已经是一个回文子串了
# 当s[i]==s[j]时,s[i:j+1]本身就是一个回文了,不需要任何插入,所以dp[i][j]=dp[i+1][j-1]
# 如果s[i]!=s[j],分情况讨论:
# 如果先把s[j]插到s[i]右边,同时把s[i]插到s[j]右边,这样得到的肯定是一个回文,但不一定是最优的
# 比如有一边是回文,如abbbb,这个时候只需要在右边添加一个a即可,
# 所以第一步,做选择,先将s[i:j] 或s[i+1:j+1]变为回文,只需判断哪个变为回文所需的操作少就变哪个
# 即判断dp[i][j-1] 和dp[i+1][j]的大小,哪个小就选哪个
# 所以,根据第一步的选择,将s[i:j+1]变为回文。
# 如果第一步中选择把s[i+1:j+1]变为回文,那么只需在s[i+1:j+1]的右边插入s[i]即可将s[i:j+1]变为回文
# 或者第一步中选择将s[i:j]变为回文,只需在s[i:j]的左边插入s[j]即可
# 所以状态转移方程为
# if s[i]==s[j]:
# dp[i][j] = dp[i+1][j-1]
# else:
# dp[i][j] = min(dp[i][j-1], dp[i+1][j]) + 1
# 前面知道,dp[i][i]=0,而dp[i][j]与dp[i][j-1], dp[i+1][j]和dp[i+1][j-1]有关
# 为了保证每次计算dp[i][j]时,这三个状态都已经被计算,我们一般选择从下向上,从左到右遍历dp数组:
# 从下往上遍历,要先得到i+1,再有i
for i in range(n - 1, -1, -1):
# 从左往右遍历,要先得到j-1,再有j
# j从i+1开始,因为j要大于等于i
for j in range(i + 1, n):
if s[i]==s[j]:
dp[i][j] = dp[i+1][j-1]
else:
dp[i][j] = min(dp[i][j-1], dp[i+1][j]) + 1
return dp[0][n-1]