Manacher算法

1,154 阅读4分钟

简介

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

该算法利用回文字符串的性质来实现高效率。它在从左到右扫描字符串时维护一个"中心点"和"右边界"。在每个位置上,它利用先前处理的位置的信息来确定以该位置为中心的回文子串的长度。

算法主要思想

Manacher算法的主要思想是避免不必要的回文子串重新比较。通过利用对称性,它可以跳过冗余的比较。该算法分为两个阶段:

阶段一:预处理

  1. 在每个字符之间以及字符串的开头和结尾插入特殊字符(例如'#')以处理奇数长度和偶数长度的回文。(注:下文为了理解省略‘#’,#1#1#2#1#1#k# --> 11211k
  2. 初始化一个数组,称为"P",用于存储以每个位置为中心的回文子串的长度。初始化其他必要的变量。

阶段二:扫描

  1. 遍历修改后的字符串中的每个字符。
  2. 使用先前计算的回文信息来确定以当前位置为中心的回文子串的长度。
  3. 如有必要,更新中心点和右边界。
  4. 如果找到更长的回文子串,则更新最大回文长度。

在算法结束时,可以从扫描阶段收集的数据中获取最大回文长度及其对应的子串。

概念名词解释

  • 回文直径:如 cabbac 的回文直径是6。
  • 回文半径:如cabbac的回文半径是3。
  • 回文半径数组P[i] :表示以i位置为中心的回文半径。
  • 最右回文边界R:最开始的值是-1,表示对给定的字符串从左->右每一个位置产生回文能够达到的最长位置。比如,对于回文11211k,0位置的R等于0,1位置的R等于1,2位置的R等于4,此时3位置的R等于1小于2位置的4,R的值保持不变,当来到位置5时,R的值更新为5。

以i位置回文半径出现的两种情况

L到R的回文中有以下情况:

i表以i为中心向两边扩充回文的中心点。

  1. i>R时,R的值随着i位置对回文的扩容在不断变化,并且将每次扩容的值保存到回文半径数组P[i] 中。
  2. i<R时,分为三种情况:由于L到R是回文,故i对应此回文中心点对称为i1
  • i1的回文半径在LR内i+P[i1]<R,故i的回文半径小于R-i,对于i位置,不需要验证的区域是i1的回文半径

  • i1的回文半径在LR外,i+P[i1]>R,由于不能判断R之后是否和小于L一样是回文,故不用验证的就是R-i的位置

  • i1的回文半径刚好在L上,i+P[i1]==R,能验证的最小为R-i,在R以后Y换成S的,则可以继续扩容。

从左到右经过一次扫描后,保留回文半径数组P[i] 的最大值,就是当前字符串的最长回文子串。

代码实现

public class manacher {
    public static int manacher(String s) {
        if (s == null || s.length() == 0) {
            return 0;
        }
        // "12132" -> "#1#2#1#3#2#"
        char[] str = manacherString(s);
        // 回文半径数组
        int[] P = new int[str.length];
        // 回文中心点
        int C = -1;
        // 讲述中:R代表最右的扩成功的位置
        // coding:最右的扩成功位置的,再下一个位置
        int R = -1;
        int max = Integer.MIN_VALUE;
        for (int i = 0; i < str.length; i++) {
            // 2*C-i 表示关于回文中心对称的点i1
            // R>i 在2*C-i 和R-i中取最小 否则只是有本身
            P[i] = R > i ? Math.min(P[2 * C - i], R - i) : 1;
            // 无论是那种情况都来进行判断,如果是第二种情况的第一和第二,直接break,第三种才进入if中
            while (i + P[i] < str.length && i - P[i] > -1) {
                if (str[i + P[i]] == str[i - P[i]]) {
                    P[i]++;
                } else {
                    break;
                }
            }
            // 当当前扩容半径超过R时,对R的值进行更新和对回文中心点进行更新
            if (i + P[i] > R) {
                R = i + P[i];
                C = i;
            }
            max = Math.max(max, P[i]);
        }
        // 半径是几,减去一之后就是对应的回文字符串长度
        return max - 1;
    }


    public static char[] manacherString(String str) {
        char[] charArr = str.toCharArray();
        char[] res = new char[str.length() * 2 + 1];
        int index = 0;
        for (int i = 0; i != res.length; i++) {
            //奇数位添加#,偶数添加数字
            res[i] = (i & 1) == 0 ? '#' : charArr[index++];
        }
        return res;
    }

    public static void main(String[] args) {
        String str = "11221";
        int manacher = manacher(str);
        System.out.println("manacher = " + manacher);
    }
}