Skip to content

🍕 manacher 算法 - 回文子串

【经典解法】: 添加虚字符串,比如 1234 变成 #1#2#3#4#,然后对每一位都往两边扩,比较回文。

【经典解法时间复杂度】: O(N^2)

核心信息:

  • 每个位置的回文半径(或回文直径)数组。
  • 一个变量 R :之前所扩的所有位置中,所到达的最右侧的回文右边界。 这个变量只会变大,初始值为 -1
  • 一个变量 C :当你取得最远右边界 R 时,对应的中心点在哪里。 所以 R 和 C 是一起更新的。

玩一下:

  • 情况 1 ,到达了最远右边界之外,此时只能以该点往两边 “暴力扩”。
  • 情况 2 ,到达的点 i 属于右边界内部。则 i 一定是在 C 与 R 之间(原因是最远右边界 R 一定是前面对 C 点暴力扩时找出来的),所以此时的点一定有一个对称点 j
    • 如果 j 的回文半径在 C 的半径之内, 则 i 点的回文半径等于 j 。这个很好理解。
    • 如果 j 的回文半径在 C 的半径之外, 则 i 点的回文半径等于 i 到 R 的距离。 下面有证明
    • 如果 j 的回文半径刚好与 C 的左边界半径压线, 则 i 直接从 R 外面继续 “暴力扩”。 这算是一个小加速,因为中间部分不需要判断了,一定回文。

证明 “如果 j 的回文半径在 C 的半径之外, 则 i 点的回文半径等于 i 到 R 的距离。”

  L                                 R
  |                                 |
X |....j....Y      C      Z....i....|P
  |                                 |
首先, X 一定等于 Y ,因为它们根据 j 对称
其次, Y 一定等于 Z ,因为他们根据 C 对称
假设 i 的回文半径大于 i 到 R 距离,说明 Z 等于 P ,此时有 X 也等于 P ,这意味着 C 的半径可以更长。
所以假设不成立

时间复杂度是 O(N) ,很好理解,因为每次循环,如果 i 在 R 内部,时间复杂度是 O(1) ,如果 i 不在 R 内部,那么也会让 R 变大。 也就是不管什么情况,每次都会离 “终点” 更近,而且不会回退。 有点类似于 KMP ,但是比 KMP 容易分析很多。

java
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;
}
int maxLcpsLength(String s) {
    if (s == null || s.length() == 0) {
        return 0;
    }
    char[] str = manacherString(s);
    int[] pArr = new int[str.length];
    int C = -1;
    int R_next = -1; // 这个 R_next 是前面解释中 R 的下一个位置, 即 C 的半径范围是 [L...C...R_next-1] R 。 这里只是方便理解才写成 R_next,实际上写成 R 也是可以的。
    int max = Integer.MIN_VALUE;
    for (int i = 0; i != str.length; i++) {

        // 这句代码,囊括了很多种情况,得到的是可以直接从哪里开始“扩”
        int j = 2*C-i
        pArr[i] =
            R_next > i
                ? Math.min(pArr[j], R_next - i)  // i 在范围之内, R_next - i 就是 i 到 R 的半径距离。 pArr[j] 是 j 的半径距离。 这个包含了挺多
                                                    // 如果 j 半径在 C 之内, 则 pArr[j] 一定小于 R_next-i
                                                    // 如果 j 半径在 C 之外或刚好压到半径, 则 i 半径至少为 R_next-i
                : 1;    // i 在 R 范围之外,则只能保证 i 的回文半径一定大于等于 1

        while (i + pArr[i] < str.length && i - pArr[i] > -1) {
            // 直接从 R 之外开始继续判断是否回文 。
            if (str[i + pArr[i]] == str[i - pArr[i]]) {
                pArr[i]++;
            } else {
                // 如果 j 半径在 C 之内, 或者 j 半径在 C 之外,都会直接退出。 具体证明前面已经说明了。
                break;
            }
        }
        // 更新 C 和 R_next
        if (i + pArr[i] > R_next) {
            R_next = i + pArr[i];
            C = i;
        }
        max = Math.max(max, pArr(i));
    }
    // 最大的回文半径 - 1,就是原字符串的最大回文直径
    return max - 1;
}