[dp14_回文串] 分割回文串 II | 最长回文子序列 | 让字符串成为回文串的最少插入次数
目录
1.分割回文串 II
题解
2.最长回文子序列
题解
3.让字符串成为回文串的最少插入次数
题解
回文串,想通过s[i] == s[j] 来实现状态变化,由二维数组 右下角 开始扩散
1.分割回文串 II
链接: 132. 分割回文串 II
给你一个字符串 s
,请你将 s
分割成一些子串,使每个子串都是回文串。
返回符合要求的 最少分割次数 。
示例 1:
输入:s = "aab"
输出:1
解释:只需一次分割就可将 s 分割成 ["aa","b"] 这样两个回文子串。
示例 2:
输入:s = "a"
输出:0
示例 3:
输入:s = "ab"
输出:1
- 在做这道题之前,可以去做一下 单词拆分那题,判断 j--i 是不是即可,是的话 dp[j-1]+1
题解
1.状态表示
这道题和 “单词拆分” 类似,需要从左往右一个一个试,所以我们可以根据经验 + 题目要求 来找出状态表示。
- 之前的题用某个位置为结尾的状态表示,推不出状态转移方程。。经验失效了,所以用的方法解决。这道题可以。
- dp[i] 表示: s [0,i] 区间上的最长的子串,最少分割次数
2.状态转移方程
- 仅需考虑[0,i]区间的子串,不需要考虑后面的。
如果[0,i]区间的子串本身就是回文了,根本不需要切割了。
- 如果[0,i]区间的子串不是回文,这个时候就想dp[i]能不能用之前的状态来表示一下,如果可以就能推出状态转移方程。
- 如果 [0,i] 区间 来一个 j 切出来一个 [j,i] 的子串,如果[j,i] 是一个回文串,接下来在 [0,j-1] 看看切多少刀,然后再加上切出来的[j,i] 这一刀就可以了。
-
我们要经常判断 0 ~ i,j ~ i 是否是回文,总体时间复杂度O(N^3)。
所以优化一下:
- 回文子串,二维 dp 表,将所有的子串是否是回文的信息,保存在 dp 表里面。
- 这样就可以以O(1)时间复杂度在 dp 表中判断是否是回文了,时间复杂度降到O(N^2)
3.初始化
- 这道题是不用初始化的,因为只有 j == 0,dp[j -1]才会越界,但是 j > 0。
- 因为只有本身的话,没什么好切割的
- 但是因为dp[i] 要找最小,如果不初始化 dp表里面都是0,选最小会有影响
- 所以 dp 表内所有的值都初始化为无穷大。
4.填表顺序
- 从左往右
5.返回值
- dp[i] 表示: s [0,i] 区间上的最长的子串,最少分割次数
- 这道题要求整个区间的最少分割次数,所以返回 dp[n-1]
找回文串的思路上一篇文章当中有讲到过
class Solution {
public:int minCut(string s) {int n=s.size();if(n==1) return 0;//先 处理 回文串vector<vector<bool>> dp(n,vector<bool>(n,false));for(int i=n-1;i>=0;i--){for(int j=i;j<n;j++) //!!!!向后移动{if(s[i]==s[j]){if(i==j)dp[i][j]=true;else if(i+1==j)dp[i][j]=true;elsedp[i][j]=dp[i+1][j-1];}}}
//双二维dp 先判bool
//再 切割次数//二维数组 表切割起始 和结束vector<int> ret(n,INT_MAX);//求最小值 初始化为 最大ret[0]=0;for(int i=1;i<n;i++){if(dp[0][i]) ret[i]=0;for(int j=1;j<=i;j++){if(dp[j][i]){ret[i]=min(ret[i],ret[j-1]+1);}}}return ret[n-1];}
};
2.最长回文子序列
链接:516. 最长回文子序列
给你一个字符串 s
,找出其中最长的回文子序列,并返回该序列的长度。
子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。
示例 1:
输入:s = "bbbab"
输出:4
解释:一个可能的最长回文子序列为 "bbbb" 。
示例 2:
输入:s = "cbbd"
输出:2
解释:一个可能的最长回文子序列为 "bb" 。
如果是 连续的子数组,我们可以很简单的想到
class Solution {
public:int longestPalindromeSubseq(string s) {//bool 判回文int n=s.size(),ret=0;vector<vector<bool>> dp(n,vector<bool>(n,false));for(int i=n-1;i>=0;i--){for(int j=i;j<n;j++){if(s[i]==s[j]){if(i==j)dp[i][j]=true;else if(i+1==j)dp[i][j]=true;elsedp[i][j]=dp[i+1][j-1];}if(dp[i][j]) ret=max(ret,j-i+1);}// 左下往右上填}return ret;}
};
那么子序列呢
题解
1.状态表示
- 关于子序列的题我们做了很多了,前面都是以 i 位置为结尾 + 题目要求分析问题
这里我们先根据之前的经验来一个状态表示
dp[i] 表示:以 i 位置元素为结尾的所有子序列中,最长回文子序列的长度。
- 以 i 位置元素为结尾,势必会用到 i 位置之前,i - 1, i - 2,i - 3。。。这种找子序列的套路可以跟在它们任意后面,根据 dp[i -1],dp[i - 2],dp[i - 3] 去填 dp[i]。
- 但是这里有个问题上面状态表示只知道前面最长回文子序列的长度
- 并不知道回文子序列是什么,加上 i 位置是否构成回文子序列。因此上面状态表示不对。
在回文子串哪里,如果以 i 位置为起点,j 位置为结束的子串是一个回文子串,i 前面加一个字符,j 后面加一个字符,如果相等,依旧是一个回文子串。
- 比如在s 字符串里面依旧选一个 i -> j 区间,如果知道这个区间内的最长回文子序列的长度,如果 i 前面字符 和 j 后面字符是一样的。
- 那也能推出来 i - 1 -> j +1 区间回文子序列的长度。在原有基础上多加一个2。因此状态表示
dp[i][j] 表示:s 字符串 [i, j] 区间内的所有子序列,最长的回文子序列的长度。
- 还是应用到了一维不行,那就在加一维表区间的思想
2.状态转移方程
如果 s[i] == s[j]
- i == j ,最长的回文子序列的长度为1
i +1 == j ,最长的回文子序列的长度为2
i + 1 < j ,现在 i + 1 -> j - 1 区间内找最长的,然后在加上 i j,最长的回文子序列的长度为dp[i + 1][j -1] + 2
如果s[i] != s[j]
- i j 一定不可能同时存在构不成回文子序列,那就去找 i + 1 -> j 区间 和 i -> j - 1区间 找找,然后取两个区间的最大值就可以了。
- 注意 i + 1 - > j - 1区间已经包括在上面的区间了,因此不用单独去找了。
3.初始化
因为 s[i] == s[j]情况分的特别细 ,所有dp[i+1][j-1]是不会越界的。
- 考虑一下 s[i] != s[j],来一个二维dp表,我们只会用到上三角,因为要保证 i <= j。
- 只有 i == j 并且 第一个位置 和 最后一个位置 dp[i][j-1] 和 dp[i+1][j] 会越界
- 但是注意 是在 i == j 的情况。因此我们可以在填表时提前特殊处理一下 i == j dp[i][j] =1。
- 所以 dp[i][j-1] 和 dp[i+1][j]这两个位置越界根本不会发生,因此表无需初始化。
4.填表顺序
- 因此,从下往上填写每一行
- 每一行从左往右填写
5.返回值
- dp[i][j] 表示:s 字符串 [i, j] 区间内的所有子序列,最长的回文子序列的长度。
- 但是我们要的是整个区间的最长的回文子序列的长度,因此返回 dp[0][n-1]。
class Solution {
public:int longestPalindromeSubseq(string s) {int n=s.size();vector<vector<int>> dp(n,vector<int>(n,0));//初始化for(int i=n-1;i>=0;i--){for(int j=i;j<n;j++){if(s[i]==s[j]){if(i==j)dp[i][j]=1;else if(i+1==j)dp[i][j]=2;elsedp[i][j]=dp[i+1][j-1]+2;}elsedp[i][j]=max(dp[i][j-1],dp[i+1][j]);}}return dp[0][n-1]; }
};
- 相当于 是从 最右下角开始填的
- s[i] 和 s[j] 的关系,为状态变化契机
- 要记得初始化DP,才能通过下标来引用
3.让字符串成为回文串的最少插入次数
链接:1312. 让字符串成为回文串的最少插入次数
给你一个字符串 s
,每一次操作你都可以在字符串的任意位置插入任意字符。
请你返回让 s
成为回文串的 最少操作次数 。
「回文串」是正读和反读都相同的字符串。
示例 1:
输入:s = "zzazz"
输出:0
解释:字符串 "zzazz" 已经是回文串了,所以不需要做任何插入操作。
示例 2:
输入:s = "mbadm"
输出:2
解释:字符串可变为 "mbdadbm" 或者 "mdbabdm" 。
示例 3:
输入:s = "leetcode"
输出:5
解释:插入 5 个字符后字符串变为 "leetcodocteel" 。
题解
1.状态表示
- 我们已经做过很多回文串的问题,因此我们还是在这个 s 字符串中 选取一段区间
i ->j 研究问题 (i <= j) - dp[i][j] 表示:s里面 [i,j] 区间内的子串,使它成为回文串的最小插入次数
2.状态转移方程
- 关于回文串这里从i -> j,我们就以这两个端点来分析问题
当s[i] == s[j]
- i == j,本身 --不用插入
i + 1 == j, 相邻--不用插入操作
i + 1 < j, i 和 j 中间有其他字符,因为 s[i] 已经等于 s[j],所以只用考虑 i + 1 -> j -1区间最小插入次数,正好就是 dp[i + 1][j - 1]
当s[i] != s[j]
- i 和 j 位置字符不相等,我想要让这个区间是回文串,必须得先让两个端点是回文串,有两个方法
- 第一种方法,可以考虑在 i 前面加一个 s[j] 然后就可以和 j 位置字符匹配, 然后让 i -> j -1区间成为回文串就行了,就是dp[i][j-1]。
- 第二种方法,可以考虑在 j 后面加一个 s[i] 然后就可以和 i 位置字符匹配, 然后让 i + 1-> j 区间成为回文串就行了,就是dp[i + 1][j]。
然后取这两种情况的最小值。
3.初始化
- 当 s[i] == s[j] 情况,分析一下dp[i + 1][j - 1] 会不会越界。
- 注意 i <= j 只填上三角,当 i == j 的时候对角第一个位置和最后一个位置会越界,但是 i == j 我们已经特殊处理了,i == j 等于 0。因此不用初始化。
还有一点 i + 1 == j ,可以和 i + 1 < j 合并,当 i + 1 == j 可以用dp[i + 1][j - 1] 填。如果在创建 dp 表 初始化就是0,i + 1 == j 放在i + 1 < j里面填,dp[i + 1][j - 1]也是0
当 s[i] != s[j]
- 肯定不会对角线,因为对角线肯定是 s[i] == s[j]。肯定是不会越界的。
4.填表顺序
- 当填 i j 发现会用到左边,左下,下边。因此从下往上每一行,每一行从左往右
- 即 最右下角 开始填
5.返回值
- dp[i][j] 表示:s里面 [i,j] 区间内的子串,使它成为回文串的最小插入次数
- 而我们要的是整个区间的最小插入次数,因此返回 dp[0][n-1]
class Solution {
public:int minInsertions(string s) {int n=s.size();vector<vector<int>> dp(n,vector<int>(n,0));for(int i=n-1;i>=0;i--){for(int j=i;j<n;j++){if(s[i]==s[j]){if(i==j)dp[i][j]=0;else if(i+1==j)dp[i][j]=0;else 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]; }
};