动态规划解最长子序列子串等一类问题之让字符串成为回文及其Follow Up[Sika Deer]
Posted 阿飞算法
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了动态规划解最长子序列子串等一类问题之让字符串成为回文及其Follow Up[Sika Deer]相关的知识,希望对你有一定的参考价值。
走过路过不要错过
点击蓝字关注我吧
这是个好题目
0.源起
1.Step1
源问题,如上图,让字符串成为回文串的最少插入次数
定义状态
n
为str
的长度,dp[n][n]
是一张二维表定义:
dp[i][j]
表示的是子串str[i][j]
最少添加多少字符可以使其整体变成一个回文,注意,是最少要求的结果是
dp[0][n-1]
表示的是str[0…n-1]
也就是这个字符从开头到结尾,要想形成回文,最少的添加字符的个数
问题来了,如何填写这张dp table呢?
分三大类讨论
1.
str[i…j]
只有一个字符,即i==j
,如图,str[i…j]=A
,一个字符要形成回文,很简单,不要向其增加字符,即其本身可以形成回文,即dp[i][j]=0
2.
str[i…j]
有两个字符2.1,当
str[i]==str[j]
,即两个字符相同时,其与场景1的情况一样,例如,AA
不需要添加字符,即可使其成为回文,即dp[i][j]=0
2.2,当
str[i]!=str[j]
,即两个字符不同时,例如,AB
,可以在前面加一个B
,变成BAB
,也可以在后面加一个A
,变成ABA
,总之是只要添加一个字符,即可使其变成回文,即dp[i][j]=1
3.
str[i…j]
有三个或者三个以上字符,这就属于一般的情况了3.1.当
str[i]==str[j]
时,例如,ABDCA
,只需要考虑BDC
,也即要求dp[i][j]
,转化为求dp[i+1][j-1]
,只要搞定str[i+1…j-1]
部分,使其变成回文,再加上str[i]
与str[j]
,整体就变成回文了,即转移方程:dp[i][j]=dp[i+1][j-1]
3.2.当
str[i]!=str[j]
时,有两种方法使其变成回文,一种是先让str[i…j-1]
变成回文,在i
的左边添加str[j]
,整体就会变成回文,例如ABECD
,可以先考虑ABEC
这部分,一个方案是ABECEBA
,再在ABECEBA
左边加上str[j]
,即D
,带上源字符的D
,变成了DABECEBAD
;另外一种方案,同理,先让str[i+1…j]
变成回文,最后在右边加上str[i]
,最终变成了整体回文,那么,动态转移方程式什么呢?要想使
str[i…j-1]
变成回文,其最少的添加次数是dp[i][j-1]
,再加上往左侧补的字符,最终应该是dp[i][j-1]+1
同理可得,
dp[i+1][j]+1
dp[i][j]=min(dp[i][j-1],dp[i+1][j])+1
核心代码
生成
dp
table
的代码,注意遍历时,综合考虑了以上分析的三种情况下文中需要使用到这个
buildDP
,做个一个抽离
public int[][] buildDP(char[] chas) {
// `dp[i][j]`表示子串`str[i...j]`范围内的最少添加多少个字符后,可以形成回文子串
int n = chas.length;
int[][] dp = new int[n][n];
for (int j = 1; j < n; j++) {
dp[j - 1][j] = (chas[j - 1] == chas[j]) ? 0 : 1;
for (int i = j - 2; i >= 0; i--) {
if (chas[i] == chas[j]) dp[i][j] = dp[i + 1][j - 1];
else dp[i][j] = Math.min(dp[i + 1][j], dp[i][j - 1]) + 1;
}
}
return dp;
}
整体代码
public int minInsertions(String s) {
int[][] dp = buildDP(s.toCharArray());
return dp[0][s.length() - 1];
}
复杂度分析
时间复杂度:
O(N^2)
,N
是字符的长度空间复杂度:
O(N^2)
,dp
表的空间
2.Step2
题目
给定一个字符串str,如果可以在str的任意位置添加字符,请返回在添加字符最少的情况下,让str整体都是回文字符串的结果。
这个问题比
Step1
的问题多了一步,要求返回一种使其变成整体回文的结果,例如,源字符AB
,返回一种添加最少字符,使其变成回文的一种方案,返回ABA
或者BAB
均可
解题思路
准备一个
res
数组,长度是字符s
本身的长度+dp[0][n-1]
运用双指针的想法,给
res
填充字符当
str[i]==str[j]
时,res
直接追加即可当
str[i]!=str[j]
时,需要找到一个最少添加字符的方案当`dp[i][j - 1] < dp[i + 1][j]`时,即最终在生成字符的时候,在左侧填充一个`str[j]`
当`dp[i][j - 1] >= dp[i + 1][j]`时,即最终在生成字符的时候,在右侧填充一个`str[i]`
整体代码
public String getPalindromeI(String s) {
char[] chas = s.toCharArray();
int n = chas.length;
int[][] dp = buildDP(chas);
int appendLen = dp[0][n - 1];
char[] res = new char[n + appendLen];
int i = 0, j = n - 1;
int left = 0, right = res.length - 1;
while (i <= j) {
if (chas[i] == chas[j]) {
res[left++] = chas[i++];
res[right--] = chas[j--];
} else {
if (dp[i][j - 1] < dp[i + 1][j]) {
res[left++] = chas[j];
res[right--] = chas[j--];
} else if (dp[i][j - 1] >= dp[i + 1][j]) {
res[left++] = chas[i];
res[right--] = chas[i++];
}
}
}
return String.valueOf(res);
}
复杂度分析
时间复杂度:
O(N^2)
,N
是字符的长度,求res
的过程是O(N)
,总体是O(N^2)
空间复杂度:
O(N^2)
,dp
表的空间
3.Step3
给定一个字符串str,再给定str的最长回文子序列字符串strlps,请返回在添加字符最少的情况下,让str整体都是回文字符串的一种结果。进阶问题比原问题多了一个参数,请做到时间复杂度比原问题的实现低
举例:
str="A1B21C",strlps="121",返回"AC1B2B1CA"或者"CA1B2B1AC",总之,只要是添加的字符数最少,只返回其中一种结果即可
题目分析
这个问题是给出了一个
seed
字符,生成最少添加字符形成回文后的其中一种结果
解题思路
运用剥洋葱的方式去解决
str
的长度为n
,strlps
的长度为m
,形成回文res
的结果是2*n-m
,因为m
部分已经是回文了,不需要额外添加以
str
=A1BC22DE1F
,strlps
=1221
为例洋葱的第0层是由
strlps[0]
与strlps[m-1]
,组成,即1…1
,从str
的最左侧开始找,一直找到strlps[0]=1
的字符为止,记为leftPart
,为A
,接着从str
的最右侧开始找,一直找到strlps[m-1]=1
的字符为止,记为rightPart
,为F
,将
leftPart+rightPart的逆序
,复制到res
的左侧将
rightPart+leftPart的逆序
,复制到res
的右侧结果变成了
AF……FA
在加上
strlps
,变成了AF1……1FA
洋葱进入第1层,即
2…2
,接着第一层的位置找str
,leftPart
为BC
,rightPart
为DE
,将
leftPart+rightPart的逆序
,复制到res
的左侧将
rightPart+leftPart的逆序
,复制到res
的右侧结果变成了
BCED……DECB
在加上
strlps
,变成了BCED2……2DECB
加上上一层,变成了
AF1BCED22DECB1FA
洋葱进入第n层。。。
代码部分注释掉了一部分,将
resLeft = 0, resRight = 0;
定义为全局变量,好理解些整体实现逻辑是双指针,游走,不过左右指针有很多对,嗯,只能是,细节是魔鬼啊
整体代码
//res array 的左右指针,定义为全局变量
int resLeft = 0, resRight = 0;
public String getPalindromeII(String str, String strlps) {
char[] chas = str.toCharArray();//str 目标字符串
char[] lps = strlps.toCharArray();//base 字符串
int n = chas.length, m = lps.length;//len
char[] res = new char[2 * n - m];//res字符串 arr
int chasLeft = 0, chasRight = n - 1;//str 的左右指针
int lpsLeft = 0, lpsRight = m - 1;//base 的左右指针
resRight = res.length - 1;
int tmpLeft = 0, tmpRight = 0;//tmp 左右指针
while (lpsLeft <= lpsRight) {//循环遍历lps字符
tmpLeft = chasLeft;
tmpRight = chasRight;
while (chas[chasLeft] != lps[lpsLeft]) chasLeft++;//如果lpsLeft在chas未出现,一直chasLeft++
while (chas[chasRight] != lps[lpsRight]) chasRight--;//如果lpsRight在chas未出现,一直chasRight--
build(res, chas, chasLeft, chasRight, tmpLeft, tmpRight);//组装
// int len = chasLeft - tmpLeft + tmpRight - chasRight;
// resLeft += len;
// resRight -= len;
res[resLeft++] = chas[chasLeft++];//添加当前的lpsLeft ==lpsRight对于的字符串
res[resRight--] = chas[chasRight--];//添加当前的lpsLeft ==lpsRight对于的字符串
lpsLeft++;//进入下一轮
lpsRight--;
}
return String.valueOf(res);
}
/**
* @param res 结果arr
* // * @param resLeft
* // * @param resRight
* @param chas str arr
* @param chasLeft str arr left pointer
* @param chasRight str arr right pointer
* @param tmpLeft tmp left pointer
* @param tmpRight tmp right pointer
*/
private void build(char[] res,
char[] chas, int chasLeft, int chasRight,
int tmpLeft, int tmpRight) {
for (int i = tmpLeft; i < chasLeft; i++) {
res[resLeft++] = chas[i];
res[resRight--] = chas[i];
}
for (int j = tmpRight; j > chasRight; j--) {
res[resLeft++] = chas[j];
res[resRight--] = chas[j];
}
}
Reference
程序员代码面试指南
本文完
关联阅读
以上是关于动态规划解最长子序列子串等一类问题之让字符串成为回文及其Follow Up[Sika Deer]的主要内容,如果未能解决你的问题,请参考以下文章