题目
最长回文子串
输入: "babad"
输出: "bab"
注意: "aba" 也是一个有效答案。
审题
分aba、abba两种形式,不能考虑不周全
暴力思路
确定任意2点,检查回文
n^3
动态规划
官方的思路是,如果a[i],a[j]相等,那么如果a[i+1]到a[j-1]是回文子串,那么a[i]到a[j]也是回文子串了。
如果a[i],a[j]不相等,那么a[i]到a[j]不论如何都不是回文子串。
用这个方法去辅助暴力法的回文子串的判断。
记录下底层的dp[i][j]是否回文帮助高层进行判断。
我的思路是在遍历到新的一位的时候,前面的回文有多种可能,遍历前面的所有可能,得出包含新的一位的所有新的可能。实际上还是和官方的思路一样。
找到回文的中间线
aba
abba
都是有中间线的。所以遍历每一位,以每一位为基点向两边延伸。
暴力代码
class Solution {
public String longestPalindrome(String k) {
if(k.equals("")) {
return k;
}
char[] chars = k.toCharArray();
int len = chars.length;
int max = 0,s = 0,e = 0;
for(int i = 0; i < len; i ++) {
for(int j = i + 1; j < len; j ++) {
if(checkPali(i, j, chars)) {
if(j - i + 1 > max) {
max = j - i + 1;
s = i;
e = j;
}
}
}
}
StringBuilder sb = new StringBuilder();
for(int i = s; i <= e; i ++) {
sb.append(chars[i]);
}
return sb.toString();
}
private boolean checkPali(int s, int e, char[] chars) {
for(int i = 0; i < (e - s + 1)/2; i ++) {
if(chars[s + i] != chars[e - i]) {
return false;
}
}
return true;
}
}
跑出来的结果是1212ms,惊了
官方动态规划代码
class Solution {
public String longestPalindrome(String k) {
if(k.equals("")) {
return k;
}
//下面的都是非空非""的,数组至少有1位
char[] chars = k.toCharArray();
int len = chars.length;
int max = 0;
int bestS = 0;
int bestE = 0;
boolean[][] dp = new boolean[len][len];//i:开始,j:结束
for(int j = 0; j < len; j ++) {//定死右点
for(int i = j - 1; i >= 0; i --) {//遍历左点 这里正反来皆可
if(chars[i] == chars[j]) {
if(i + 1 == j || i + 2 == j) {
dp[i][j] = true;
} else {
if(dp[i+1][j-1]) {
dp[i][j] = true;
} else {
dp[i][j] = false;
}
}
if(dp[i][j] && (j-i+1) > max) {
max = j - i + 1;
bestS = i;
bestE = j;
}
} else {
dp[i][j] = false;
}
}
}
StringBuilder sb = new StringBuilder();
for(int i = bestS; i <= bestE; i ++) {
sb.append(chars[i]);
}
return sb.toString();
}
}
动态规划原理分析
官方的:i->j。只有a[i]==a[j]且dp[i+1][j-1] = true,才会回文。
我的:向右遍历到i,对于回文串的右端定在i-1的进行判断,看是否另一端相等,如果是,在这个级别上增加回文。
我和官方的区别在于,我的是找到上个级别的所有回文,以及当前级别的数,再去找到首端的数进行比较;官方的是先判断首尾端的数相等不相等,如果相等,再看内部是否回文。
中间线
分aba、abba两种形式处理
class Solution {
public static String longestPalindrome(String k) {
if(k.equals("")) {
return k;
}
//下面的都是非空非""的,数组至少有1位
char[] chars = k.toCharArray();
int len = chars.length;
int max = 0;
int bestS = 0;
int bestE = 0;
for(int i = 1; i < len - 1; i ++) {//aba形式
//只管遍历,到头了为止。中间线在i的位置。
int s = i-1;
int e = i+1;
while(s >= 0 && e <= len - 1) {
if(chars[s] != chars[e]) {
break;
}
s--;
e++;
}
//从while出来,要么不相等,要么越界,需要校准
s++;
e--;
int l = e-s+1;
if(l > max) {
max = l;
bestS = s;
bestE = e;
}
}
for(int i = 0; i < len - 1; i ++) {//abba形式
//i分割了i i+1
int s = i;
int e = i+1;
while(s >= 0 && e <= len - 1) {
if(chars[s] != chars[e]) {
break;
}
s--;
e++;
}
//从while出来,要么不相等,要么越界,需要校准
s++;
e--;
int l = e-s+1;
if(l > max) {
max = l;
bestS = s;
bestE = e;
}
}
StringBuilder sb = new StringBuilder();
for(int i = bestS; i <= bestE; i ++) {
sb.append(chars[i]);
}
return sb.toString();
}
}
写完之后才发现这两种处理可以merge成一种处理的。有点尴尬。是我一开始没有想完善。奇偶也通常都可以merge的。
时间复杂度比动态规划优化了30%,这是可以理解的。官方动态规划是从2点出发,每两个点之间是否回文都被验证了一遍,可以说官方动态规划是稳定的n^2。而中间线思路,从中间线开始延伸,遇到不相等的就不继续了,是不稳定的,但是肯定是不足n^2的。最坏是所有的数全相等,这才是n^2。
翻转思路
这是官方的思路。把A翻转,得到B,再计算他们的最长公共子串就可以了。
比如"caba"-->"abac",得出答案aba。
但是"abcdkxdcba"-->"abcdxkdcba",得出答案abcd,显然这不是回文。
为何会有这样的误区呢?对于回文的字符aba,翻转后必然也是aba。但是对于不能回文的,也有些字符可以浑水摸鱼。浑水摸鱼的原因是回文串翻转后还是和自己比,这里的情况是翻转后和实现准备好的翻转副本比较。
解决方法很简单,把得到的结果,最后再check一次是否回文即可。
思路是很好的,有时候数组翻转可以解决很多问题!!!
这样一来问题就变成了怎么求最长公共子串了,这又是一个难题,最朴素的实现是,遍历a数组,每一位都去b数组中找相同的,找到相同的时候继续比。然后a数组继续遍历。。。复杂度大概是n^3,用上hashmap存储index,可以降到n^2,a数组当前遍历的值,直接去存储b数组的hashmap里找这个数的index,然后继续遍历。
还有个思路是偏移某个数组,进行直接映射比较,也是n^2。
也可以用动态规划,这样空间复杂度会从1变到n^2。a[i],b[j]分别是a中的某个字符,b中的某个字符。dp[i][j]是在a中以i结尾,b中以j结尾的最长串,那么dp[i+1][j+1]就可以因此递推。时间上的优化是因为dp保存了之前比较的结果,因此快了,这也是动态规划的原理。
空间复杂度用滚动数组可以优化到n。
详细见https://www.cnblogs.com/ider/p/longest-common-substring-problem-optimization.html
Manacher
学习自https://blog.csdn.net/ggggiqnypgjg/article/details/6645824
这个算法挺容易理解的。算法基于中心算法。
mx之前求得的回文串的右端最远距离,id则是这个回文串的中心。
当前要求的是i的半径。i'是ii关于id的映射。可以看到i'的半径和i的半径息息相关。
假如i'的半径没有超出mx'的界限,那么毫无疑问,i的半径和i'的半径是一样的。
如果i'的半径超出了mx'的界限,那么,i的半径就是mx-i?
当然不是。i的半径暂时是mx-i了。这种越界的情况,需要继续到mx后面去正儿八经的匹配。
上面的这种做法就是避免了不必要的判断,充分地利用了回文的特性去剪枝。
所以得出了这行代码
全部代码如下:
噢,对了,为了同化奇数和偶数的处理,这个算法还给原字符加上了乱七八糟的字符。这里我就不这么做了,还是分开处理他们。
public static String longestPalindrome(String k) {
if(k.equals("")) {
return k;
}
int max = 0;
int bestS = 0;
int bestE = 0;
char[] chars = k.toCharArray();
int len = chars.length;
int id = 0;
int mx = 0;
int[] cache = new int[len];
//aba
for(int i = 1; i < len - 1; i ++) {//两端就不遍历了,没有必要
//剪枝
if(mx > i && i > id) {//i处在mx和id中间
int j = 2 * id - i;//j一定是合法的,不可能小于0
cache[i] = Math.min(cache[j], mx - i);
} else {
cache[i] = 0;
}
//遍历
int s = i-cache[i]-1;
int e = i+cache[i]+1;
while(s >= 0 && e < len && chars[s] == chars[e]) {
s--;
e++;
cache[i]++;
}
s++;//越界或不等,复位
e--;
//记录mx、id
if(e > mx) {
mx = e;
id = i;
}
if(cache[i] > max) {
max = cache[i];
bestS = s;
bestE = e;
}
}
//abba
int[] cache2 = new int[len];
int id2 = 0;
int mx2 = 0;
for(int i = 0; i < len - 1; i ++) {
//i分割i i+1
if(mx2 > i && i > id2) {
int j = 2*id2-i;
if(j >= 1) {
cache2[i] = Math.min(cache2[j], mx2-i);
}
} else {
cache2[i] = 0;
}
//遍历
int s = i-cache2[i];
int e = i+cache2[i]+1;
while(s >= 0 && e < len && chars[s] == chars[e]) {
s--;
e++;
cache2[i]++;
}
s++;//复位
e--;
//记录mx、id
if(e > mx2) {
mx2 = e;
id2 = i;
}
if (cache2[i] > max) {
max = cache2[i];
bestS = s;
bestE = e;
}
}
StringBuilder sb = new StringBuilder();
for (int i = bestS; i <= bestE; i ++) {
sb.append(chars[i]);
}
return sb.toString();
}
代码很渣。
为什么是N
因为i如果存在mx里,就会被找到映射,映射的最优也就是i的最优。i继续向下遍历,同时也会刷新mx。所以时间复杂度是跟你这mx走的,显然是n。
收获
从官方的动态规划有收获
1.思路:和往常的往右遍历,分析出转移不一样。它是考虑了两端,没有被数组限制。
2.做法:由于和常规dp的思路不一样,所以代码也不一样。往常是正常遍历,显然在这里行不通,这里是高层需要借助底层的东西,可以用递归,这里是定死右界,产生底层的数,给高层用的。
马拉车算法也很厉害,充分利用回文的特性剪枝。