继续补充一些题目简单但思路丰富的题目,很有标志性的字符串回文问题。学习了一下Manacher算法。
题目(LeetCode No.5)

中心扩展法
首先是最简单思路,时间复杂度为O(n^3)的暴力法。选择所有能出现的字符子串的集合,依次检验是否为回文串,然后取长度最大者。共有 n(n-1)/2 个不同的子串,验证子串消耗为O(n)。
我觉得第二好理解的思路其实是中心扩展法。相比动态规划来说,中心扩展的思路只要明白回文左右互为镜像,在一次循环遍历中将每一个经过的点当做回文的中点,然后向左右两边伸展判断即可。在开头、结尾和字符串的每个字符之间插入‘#’可以无视子字符串是奇数还是偶数个。(‘#’可以是任意字符串中不会出现的符号。)
因为很直白所以直接给出Leetcode的题解。
public String longestPalindrome(String s) {
if (s == null || s.length() < 1) return ""; //非空判定
int start = 0, end = 0;
for (int i = 0; i < s.length(); i++) {
int len1 = expandAroundCenter(s, i, i); //对中心为奇和偶都进行讨论
int len2 = expandAroundCenter(s, i, i + 1);
int len = Math.max(len1, len2);
if (len > end - start) { //判断是否为当前最长子串,是则记录下标
start = i - (len - 1) / 2;
end = i + len / 2;
}
}
return s.substring(start, end + 1);
}
//判断以i或(i和i+1)为中心的子串的长度
private int expandAroundCenter(String s, int left, int right) {
int L = left, R = right;
while (L >= 0 && R < s.length() && s.charAt(L) == s.charAt(R)) {
L--;
R++;
}
return R - L - 1;
}
动态规划
动态规划其实就是暴力遍历的优化。无论是动态规划还是Manacher算法,核心思路就是最大化利用已求出过的结论,减少重复计算,无非是Manacher算法的利用效率比动态规划更高。
使用DP解题没有思路时,首先找三要素:
最优子结构、边界、状态转移方程
对于任何一个回文串 S(i , j),其 S(i+1 , j-1)一定为 回文串。反之:
S (i , j) = S(i+1 , j-1) && Si==Sj
边界即为S(i+1 , j-1)为一个字母或两个字母的回文串。
class Solution {
public String longestPalindrome(String s) {
int len = s.length();
String answer = "";
boolean[][] dp = new boolean[len][len];
for(int i = 0 ;i < len; i++){
for(int j = i; j >= 0 ;j--){
if(s.charAt(i) == s.charAt(j)&&(i-j < 2 || dp[i-1][j+1]))
dp[i][j] = true;
if (dp[i][j] && (i-j+1 > answer.length())){
answer = s.substring(j,i+1);
}
}
}
return answer;
}
}
Manacher算法
最后是Manacher算法。看完算法思路之后我写了一个我自己觉得更容易理解的版本,代码量会稍微多几行。
class Solution {
public String longestPalindrome(String s) {
if(s.length()==0){ //非空判定
return "";
}
int maxlen = 0, c = 0, max = 0, index = 0;
int len = 2*s.length()+2;
int[] p = new int[len]; //p数组记录每一个点的回文半径
char[] chars = new char[len];
chars[0] = '$'; //头插入'$'符号处理符号组
for(int i=0;i<s.length();i++){ //每个字符之间插入'#'符号方便统一奇偶
chars[2*i+1] = s.charAt(i);
chars[2*i+2] = '#';
}
for(int i=1;i<len;i++){ //当前下标以越过曾经处理过的最远端,
if(i>=maxlen){ //即之后的位置并未到达过
p[i] = getR(i,chars,1); //未到达所以需要从半径1开始左右对比
}
else{ //当前下标在曾经到达过的字符串里
int r = Math.min(p[2*c-i],maxlen-i);//直接利用记录过的回文半径
p[i] = getR(i,chars,r);//能直接利用的极限是曾经到达过的最远端
}
if(p[i]+i>maxlen){
c = i; //更新当前延展到最远端的回文串的中心下标
maxlen = p[i]+i;//更新到达过最远端的极限
}
}
for(int i=0;i<len;i++){ //寻找回文半径最长的子字符串的中心下标
if(p[i]>max){
max = p[i];
index = i;
}
}
String answer = s.substring((index-max+1)/2,(index+max)/2);
return answer;
}
//getR函数为对中心下标为i的子字符串进行左右延伸,计算它的最大回文半径
//r则为当前可直接跳至的回文半径(因为<r的部分已处理过,所以可以直接跳)
public int getR(int i,char[] chars,int r){
int left = i-r;
int right = i+r;
int R = r;
if(left<0||right>=chars.length){
return R;
}
while(chars[left]==chars[right]){
R++;
left--;
right++;
if(right>=chars.length||left<0){
break;
}
}
return R;
}
}
递交结果
