【搞定算法】Manacher 马拉车算法

求一个字符串中的最长回文子串,这是一道经典的面试题目,解法有很多,详细可见:最长回文子串问题。其实个人感觉 Manacher 算法代码实现还是有一定难度的,真正在做题目的时候采用的可能性不是很大,但是由于 Manacher 算法求解回文子串方面的时间复杂度为 O(N),所以了解其思想还是很有必要的,coding 能力比较强的话,采用 Manacher 算法解决最长回文子串问题更是最合适不过了。

回文串的概念:一个字符串正着读和倒着读都是一样的,比如:abba、noon 等;

最长回文子串:一个字符串的最长回文子串即为这个字符串的子串中,是回文串的最长的那个。

 1、暴力解法

思路:对字符串遍历每一个字符,以对每一个字符,以它为中心,往两边扩,找出以该字符串为中心的回文串大小。这里要分为两种情况,一种是回文串长度是奇数的情况,另一种是回文串长度是偶数的情况,奇数直接扩能找出回文,偶数则不可以。为了解决这样的奇偶差异,可以在每个数字的前后加上一个虚轴 “#”。

2、Manacher 解法

思路:和暴力解法一样,都是每一个字符往两边扩,只是利用前面的结果加速了这个过程,对每个字符,都从它至少的回文半径大小开始往两边扩。

  • 相关技巧前提:

1、首先,Manacher 算法提供了一种巧妙地办法,将长度为奇数的回文串和长度为偶数的回文串一起考虑,具体做法是,在原字符串的每个相邻两个字符中间插入一个分隔符,同时在首尾也要添加一个分隔符,一般情况下可以用#号,新字符串长度为原本的 2 倍 + 1。下面举一个例子:

【搞定算法】Manacher 马拉车算法_第1张图片

2、回文半径数组 pArr:记录每个位置往两边扩,能扩出来的回文半径大小【后面的就能利用前面的进行加速】;

3、R:所有回文半径中最靠右的位置【所有回文右边界中最大的一个】【每个字符往两边扩下,最右到了什么位置,就是该位置的回文右边界】;

4、C:取得最大回文右边界时对应的中心位置。

  • 求 i 位置回文半径的步骤分析:

情况1:i 在 R 外(i >= R):i 不在最大回文右边界里,那么无法利用之前的结论,i 位置的值只能通过往两边暴力扩得到。

情况2:i 在 R 内:【则可以利用之前已有的结果进行加速,i 位置的回文半径至少为 min(R - i, pArr[i'])】(i‘ 与 i 关于 C 对称,L 和 R 关于 C 对称)

2.1、(i’ -  pArr[i'])>  L :以 i' 为中心的回文字符串完全在 (L, R)范围内,则 i 位置的回文半径大小 = pArr[i'] 【其右边界小于 R 】;

2.2、(i’ - pArr[i'])= L:以 i' 为中心的回文字符串左边界压线 L,则 i 位置的回文半径大小 >= pArr[i'],即 >= R - i,至于是否能扩得更大,需要去暴力尝试【其回文右边界至少是 R,或许大于 R】;

2.3、(i’- pArr[i'])< L :以 i' 为中心的回文字符串左边界超出(L, R)范围,则 i 位置的回文半径大小 = R - i 【其右边界就是 R】。

【搞定算法】Manacher 马拉车算法_第2张图片

public class Manacher {

    public static int maxLcpsLength(String str){
        if(str == null || str.length() == 0){
            return 0;
        }
        // 把字符串处理为manachar类字符串,1221->#1#2#2#1#
        char[] chars = manacherString(str);
        int[] pArr = new int[chars.length];  // 回文半径数组
        int C = -1;  // 取得R时的回文中心
        int R = -1;  //  R-1为最大回文右边界,R是最大回文右边界的下一位
        int max = Integer.MIN_VALUE;
        //遍历每一个字符,计算以该字符为中心的回文字符串长度
        for(int i = 0; i < chars.length; i++){
            //况一: R <= i ,i 彻底在回文右边界的右侧,回文半径至少为 1 (它本身)
            //况二: R > i,
            //得到 i 位置回文半径至少的长度
            pArr[i] = R > i ? Math.min(R - i, pArr[C - (i - C)]) : 1;  // //C-(i-C)即 i'
            //从可能扩得更远的位置开始验证
            while(i + pArr[i] < chars.length && i - pArr[i] > -1){
                if(chars[i + pArr[i]] == chars[i - pArr[i]]){
                    pArr[i]++;  // 回文半径增大
                }else{
                    break;   // 已经得到该位置的回文半径了
                }
            }
            if(i + pArr[i] > R){
                R = i + pArr[i];
                C = i;
            }
            // 此回文半径是否比max大,大就替换,否则保持不变不替换。并没有求最大回文字符串是哪一个
            max = Math.max(max, pArr[i]);
        }
        // 返回最大回文字符串长度,因为我们的chars是改造过的,是原字符串的 2倍+1
        // 从中心开始,每个字符后面有一个#,即相当于*2,但中心字符只有一个,所以要-1
        return max - 1;
    }

    public static char[] manacherString(String str){
        char[] chars = str.toCharArray();
        char[] res = new char[chars.length * 2 + 1];
        int index  = 0;
        for(int i = 0; i < res.length; i++){
            res[i] = (i & 1) == 0 ? '#' : chars[index++];
        }
        return res;
    }

    public static void main(String[] args) {
        String str1 = "abcdcba";
        System.out.println(maxLcpsLength(str1));
    }
}

【搞定算法】Manacher 马拉车算法_第3张图片

你可能感兴趣的:(左神算法,数据结构与算法)