LEETCODE (6/9) Manacher(Java)

继续补充一些题目简单但思路丰富的题目,很有标志性的字符串回文问题。学习了一下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;
    }
}

递交结果