Manacher算法

Posted by 令德湖周杰伦 on 05-26,2024

1. 介绍

Manacher算法是一种线性时间复杂度的算法,用于查找给定字符串中的最长回文子串。 它由计算机科学家G. Manacher在1975年开发。 该算法在字符串处理中被广泛应用,并在模式匹配和数据压缩等领域有应用。 该算法利用回文字符串的性质来实现高效率

2. 原理

在介绍算法原理之前了解几个概念:

  1. 暴力解
  2. 最长回文直径
  3. 最长回文半径
  4. 回文半径数组
  5. 最右回文右边界
  6. 最右回文右边界的中点

以下s表示给定的字符串

2.1 暴力解

思路:
从每一个位置i开始都从两边扩,即比较s[i-1]和s[i+1]的位置是否相等

时间复杂度为:n^2

存在的问题:如果s的长度为偶数时,会出错,例如s=abba,通过上述方法,那么最长的回文字符串长度为1,但其实是4,因为偶数时回文字符串的对称轴为虚轴

解法:

  1. 对字符串特殊处理,即在s的每一个字符的前后都加入#,那么经过处理后变为str = #a#b#b#a#,
  2. 特殊字符可以为任何字符,如果s中存在#字符也不影响最后的结果,因为在对称比较时也是“实和实”、“虚和虚”在比较,不存在“实和虚”的比较,所以不会影响结果。

以下用str代表经过特殊字符处理后的字符串

2.1 最长回文直径 和 最长回文半径

每一个位置的最长回文直径,例如:[#a#1#2#1#b#]

  • index=0 位置的最长回文直径为1,因为最长回文字符串就是#自己,最长回文半径为1,为#
  • index=1 位置的最长回文直径为3,因为最长回文字符串为#a#, 最长回文半径为2,为#a 或者 a#
  • index=5 位置的最长回文直径为7,因为最长回文字符串为#1#2#1#, 最长回文半径为4,为#1#2 或者 2#1#

2.2 最长回文半径数组

Manacher算法的精髓就是利用了最长回文半径数组来加速了整个过程。
最长回文半径数组:pArr[i], 记录了字符串srt每个位置的最长回文半径,那么字符串的最长回文子串就是str[ i - pArr[i] ~ i + pArr[i]]

2.3 最右回文右边界

  • 使用R来表示最右回文右边界,含义为:在之前所有扩的位置中,所到达的最右回文右边界。
  • 使用C来表示取得最右回文右边界时的中心点位置(必须是R更新时,C跟着更新)
  • 使用L来表示当前扩出的最长回文区域的左边界,那么最长回文区域为[L,R]

例如:str = #1#2#2#1# ,初始R=-1,C=-1

  1. index=0时,该位置扩的最长回文区域为[#], 所以R=0,C=0
  2. index=1时,该位置扩的最长回文区域为[#1#],所以R=2,C=1
  3. index=2时,扩不动,即该位置的最长回文区域为[#],R不变,C不变
  4. index=3时,该位置扩的最长回文区域为[#2#],R变大了,R=4, C=3
  5. index=4时,该位置扩的最长回文区域为[#1#2#2#1#],R变大了,R=8, C=4
  6. ...

3. 算法实现

3.1 算法步骤

  1. 当i不在最右回文右边界里,暴力扩,即 i < R
  2. 当i在最右回文右边界里
    1. i对应中心点C对称的位置 i', i'的回文区域在[L-R], 判断条件为:i' - pArr[i'] > L,此时i位置的最长回文半径就是i'的最长回文半径,即pArr[i] = pArr[i']
    2. i'的回文区域的左边界超出了L, 判断条件:i' - pArr[i'] < L,此时i位置的最长回文半径为 R-i
    3. i'的回文区域的左边界 刚好压在了 L上,判断条件:i' - pArr[i'] = L,此时 pArr[i'] = R-i,需要看下i+1的位置是否也能加入的最长回文区域中,即往i+1的位置扩才能确定

3.2 代码实现

普通版:

public int getLongestPalindromeSubUpgrade(String s){
        //1. 特殊处理
        String str = manacherStr(s);
        int pArr[] = new int[str.length()];
        int max = Integer.MIN_VALUE;
        int r = -1;
        int c = -1;
        //2. 分情况,构建 pArr[i]
        for (int i = 0; i < str.length(); i++) {
            if(i>=r){
                //暴力扩
                while (i + pArr[i] < s.length() && i - pArr[i] > -1){
                    if(str.charAt(i + pArr[i]) == str.charAt(i - pArr[i])){
                        pArr[i]++;
                    }else {
                        break;
                    }
                }
                //更新R,C
                if(i + pArr[i] > r){
                    r = i + pArr[i];
                    c = i;
                }
            }else{
                int i1 = 2 * c -i;
                int l = 2 * c - r;
                if(i1 - pArr[i1] > l){
                    pArr[i] =  pArr[i1];
                }else if(i1 - pArr[i1] < l){
                    pArr[i] =  r - i;
                }else {
                    //暴力扩
                    while (i + pArr[i] < s.length() && i - pArr[i] > -1){
                        if(str.charAt(i + pArr[i]) == str.charAt(i - pArr[i])){
                            pArr[i]++;
                        }else {
                            break;
                        }
                    }
                    //更新R,C
                    if(i + pArr[i] > r){
                        r = i + pArr[i];
                        c = i;
                    }
                }
            }
            max = Math.max(max, pArr[i]);
        }
        //3. 由于str是特殊处理的,所以返回max-1就是s的最长回文直接
        return max-1;
    }


    private String manacherStr(String s){
        char[] str = new char[s.length() * 2 + 1];
        int index = 0;
        for (int i = 0; i < str.length; i++) {
            str[i] = (i&1) == 0 ? '#' : s.charAt(index++);
        }
        return new String(str);
    }

精简版:

 public int gethwSubLongestUpgrade(String s){
        //1. 特殊处理
        String str = manacherStr(s);
        int pArr[] = new int[str.length()];
        int max = Integer.MIN_VALUE;
        int r = -1;
        int c = -1;
        //2. 分情况,构建 pArr[i]
        for (int i = 0; i != str.length(); i++) {
            int i1 = 2 * c - i;
            //统一处理,i至少的回文区域先给pArr[i]
            pArr[i] = r > i ? Math.min(pArr[i1], r-i) : 1;
            //统一暴力扩,2.1,2.2的情况也扩不动不影响
            while (i + pArr[i] < s.length() && i - pArr[i] > -1){
                if(str.charAt(i + pArr[i]) == str.charAt(i - pArr[i])){
                    pArr[i]++;
                }else {
                    break;
                }
            }
            //更新R,C
            if(i + pArr[i] > r){
                r = i + pArr[i];
                c = i;
            }
            max = Math.max(max, pArr[i]);
        }
        //3. 由于str是特殊处理的,所以返回max-1就是s的最长回文直接
        return max-1;
    }


    private String manacherStr(String s){
        char[] str = new char[s.length() * 2 + 1];
        int index = 0;
        for (int i = 0; i < str.length; i++) {
            str[i] = (i&1) == 0 ? '#' : s.charAt(index++);
        }
        return new String(str);
    }

4. 题目

4.1 最长回文子串

给你一个字符串 s,找到 s 中最长的 回文子串。

4.2 代码

 public String longestPalindrome(String s) {
        if(s == null || s.length() == 0){
            return "";
        }
        if(s.length() == 1){
            return s;
        }
        //1. 特殊处理
        String str = manacherStr(s);
        int pArr[] = new int[str.length()];
        int r = -1;
        int c = -1;
        //2. 分情况,构建 pArr[i]
        for (int i = 0; i != str.length(); i++) {
            int i1 = 2 * c - i;
            //统一处理,i至少的回文区域先给pArr[i]
            pArr[i] = r > i ? Math.min(pArr[i1], r-i) : 1;
            //统一暴力扩,2.1,2.2的情况也扩不动不影响
            while (i + pArr[i] < str.length() && i - pArr[i] > -1){
                if(str.charAt(i + pArr[i]) == str.charAt(i - pArr[i])){
                    pArr[i]++;
                }else {
                    break;
                }
            }
            //更新R,C
            if(i + pArr[i] > r){
                r = i + pArr[i];
                c = i;
            }
        }
        //3. 遍历pArr[i]
        String res = "";
        int max = Integer.MIN_VALUE;
        for(int i= 0; i< pArr.length; i++){
            if(pArr[i]> max){
                max = pArr[i];
                res = str.substring(i - pArr[i] + 1, i + pArr[i] - 1);
            }
        }
        return res.replaceAll("#", "");
    }
     private String manacherStr(String s){
        char[] str = new char[s.length() * 2 + 1];
        int index = 0;
        for (int i = 0; i < str.length; i++) {
            str[i] = (i&1) == 0 ? '#' : s.charAt(index++);
        }
        return new String(str);
    }