Manacher(马拉车)算法详解、证明和练习题

25 篇文章 0 订阅
11 篇文章 0 订阅

这个算法是用来求一个字符串中的最长回文串的长度的,且时间复杂度是O(N)。

Manacher算法的预备概念

1. 最长回文半径长度数组

  • 以字符串每个位置为中心向外扩散得到的最长的回文半径长度,例如有字符串abcbdadb,那么它的回文半径数组即为: [1, 1, 2, 1, 1, 3, 1, 1]

2. 最右回文右边界

  • 直接看例子: 有字符串0123210,那么对于每个位置来说,最右回文右边界如下:
    • 初始时最右回文右边界可置为-1,对于位置0,最右回文右边界变为1
    • 位置1,最右回文右边界为2
    • 位置2,最右回文右边界为3
    • 位置3,最右回文右边界为7 (因为直到字符串结束都是以位置3为中心的回文)
    • 位置4,最右回文右边界仍然为7
    • 位置5,最右回文右边界为7
    • 位置6,最右回文右边界为7

3. 最右回文右边界的最早到达点

  • 2是伴生的概念,对于2中例子来说,最右回文右边界为7的最早到达点就是位置3

Manacher算法

对于一个字符串,它的每一个位置i和最右回文右边界之间的关系只可能有两种情况:1. i不在最右回文右边界里; 2. i在最右回文右边界里

为了方便,将最右回文右边界即为R。

  • i不在最右回文右边界R里,此时没有什么加速的技巧,暴力扩张即可。

    • 例子:有字符串 # 1 # 2 # 1 #,开始时,R为-1,而i=0,所以i不在R里。此时看看0位置能扩到哪,但它也只能扩到自己了,因为左面没有了嘛。所以0位置结束后,R由-1变为0;接下来i=1,此时R=0,i仍然不在R里,所以看看1位置能扩到哪,我们发现可以扩到2位置,因为 # 1 # 是以1位置为中心的最长回文串 。所以位置1结束后,R变为3;i=2,此时i在R内,这时就可以先不必暴力扩张,先看看能不能加速这个过程,这就是下面要说的情况2:i在最右回文右边界里
  • i在最右回文右边界里,此时又可细分为如下三种情况:

    为了便于说明,将最右回文右边界的最早到达点记为C,那么C的最长回文子串的右边界即为R,左边界记为L。

    • i关于C对称点最长回文串在[L, R]内,例子见下图:
      在这里插入图片描述

C是最右回文右边界的最早到达点,L与R是以C为中心的最长回文子串的左右边界。对于i位置,由于它在R内,所以不必急着暴力扩张,先看看关于C的对称点i’的回文区域是啥,那i’的回文区域怎么得到呢?用之前求出的最长回文半径长度数组即可得到。上图是i’的回文区域在[L, R]内的情况。此时i的回文区域和i’的相同,为啥呢?证明如下:

在这里插入图片描述
先证[r’, l’]为回文:设i’的回文区域是红色范围,左边界为l,右边界为r,r’是r关于C的对称点,l’同理。由于[l, r]是回文,而[r’, l’]是[l, r]的对称点,因此前者是后者的逆序,即[r’, l’]也为回文。

再证i的最大回文区域就是[r’, l’]:y是l的左边一位元素,x是r右边一位元素,x’与y’是x和y关于C的对称点。当时计算i’的回文区域时,为啥没把y和x考虑进去呢?肯定是他俩不相等啊,所以x’和y’也不相等,即i的最大回文区域就是[r’, l’]。

一个例子:字符串zkabatFtabakY,C为F,那么左右边界为:z[kabatFtabak]Y,设i为倒数第四个位置(b),所以i关于C的对称点i’在正数第4个位置(b),而i’的最长回文区域是[aba],在[L, R]里(L在第一个k位置,R在第二个k位置),所以i的回文区域即为[aba],那么i的最长回文半径长度数组的值即为2(aba,半径为ab/ba,故为2)。

  • i关于C对称点最长回文串在[L, R]外,例子见下图:
    在这里插入图片描述

此时i关于C的对称点i’的最长回文串超过了C的最长回文串边界L,那么i的最长回文区域也不用算,半径就是[i, R],证明如下:

在这里插入图片描述
做L关于i’的对称点L’,R’同理。所以[L, L’]是回文,[R, R]也是回文,且[R, R]是[L, L’]的逆序。

由于C的最长回文区域是[L, R],并不是[X’, X],所以X’不等于X,否则最长回文区域就是[X’, X]了,而X’与Y’关于i’对称且在i’回文区域内,所以X’=Y‘,而Y是Y’关于C的对阵点,所以Y‘=Y,综上可以得出X不等于Y,即这种情况下i的最长回文区域只能是[i, R]。

一个例子,字符串abcdcbatttabcdcF,C为第二个t,那么其最长回文区域是:ab[cdcbat t tabcdc]F,设i到了倒数第三个位置,那么其对称点i’在正数第四个位置,而i‘的最长回文区域为abcdcba,超出了C的最长区域,因此i位置的最长回文为cdc,即回文半径为2。

  • i关于 C对称点最长回文串边界正好在L上,例子见下图:

在这里插入图片描述
此时i的最长回文区域至少是[i, R],但能否更长还要看R后面的元素都是啥,也就是说这种情况下需要暴力扩张。

总结

Manacher算法可以分为两种大情况,分别是当前索引i在R内和不在R内,若不在R内,直接暴力扩张。

若在R内,根据i的对阵点i’最长回文区域的大小又可分为三种小情况:

  • i‘的最长回文区域仍然在C的最长回文区域内,此时i的最长回文区域和i’相同;
  • i‘的最长回文区域在C的最长回文区域外,此时i的最长回文区域是[i, R];
  • i‘的最长回文区域正好在C的最长回文区域上,此时i的最长回文区域至少是[i, R],能否更长要用暴力扩张试试看。

代码:

public class Manacher {
    private char[] manacherString(String str) {
        char[] chars = str.toCharArray();
        char[] manacherStr = new char[2 * str.length() + 1];
        int index = 0;
        for (int i = 0; i != manacherStr.length; i++) {
            manacherStr[i] = (i & 1) == 0? '#': chars[index++];
        }
        System.out.println(Arrays.toString(manacherStr));

        return manacherStr;
    }

    public int manacher(String str) {
        if (str == null || str.length() == 0) {
            return 0;
        }

        char[] charArr = manacherString(str);
        int[] pArr = new int[charArr.length];
        int C = -1;
        int R = -1;
        int max = Integer.MIN_VALUE;

        for (int i = 0; i < charArr.length; i++) {
            pArr[i] = i < R? Math.min(R - i, pArr[2*C-i]): 1;
            while (i + pArr[i] < charArr.length && i - pArr[i] > -1) {
                if (charArr[i + pArr[i]] == charArr[i - pArr[i]]) {
                    pArr[i]++;
                }else {
                    break;
                }
            }
            if (i + pArr[i] > R) {
                R = i + pArr[i];
                C = i;
            }
            max = Math.max(max, pArr[i]);
        }
        return max;
    }

    public static void main(String[] args) {
        System.out.println(new Manacher().manacher("abcxyzfzyx"));
    }
}

对上述代码的说明:

  • pArr[i] = i < R? Math.min(R - i, pArr[2*C-i]): 1;这句就是将前面说的大情况小情况都放在一起了,如果i大于等于R,那么最长回文半径数组就先为1,然后暴力扩张;如果i小于R,此时要看R-i更小还是i的对称点i‘的最长回文区域更小,哪个小i的最长回文半径就是哪个,注意,虽然后面紧跟着暴力扩张的代码,但若是无法扩张代码也无法执行,所以虽然R-i更小或i‘的最长回文区域更小都可以直接出答案,但上图代码更简洁。而R-i更小和i‘的最长回文区域相等则仍需要暴力扩张。

练习题 给你一个字符串,让你在末尾添加最少数量的字符,使得添加后的字符串为回文串

关于这道题,可以用Manacher算法的思想来做。

首先找到以给出的字符串最后一个字符结尾的最长回文串,然后从头开始,遇见最长回文串的开头就停止,将这段字串逆序贴在整个字符串的末尾就是答案,举个例子:

  • 假设给出的字符串是:abcxyzfzyx,可以看出以最后一个字符结尾的最长回文串是xyzfzyx,然后从字符串的头开始遍历,直到x的前一个位置,最后遍历得到的子串为abc,将abc逆序为cba贴在给出的字符串后面即为答案:abcxyzfzyxcba

只需对Manacher算法做一点改进: 有边界第一次到字符串末尾时就跳出,此时得到的即为以最后一个字符结尾的最长回文串,注意,他不一定是所有回文串中最长的,但这没关系,我们要的是以最后一个字符结尾的最长回文串!!!

public class Manacher {
    private char[] manacherString(String str) {
        char[] chars = str.toCharArray();
        char[] manacherStr = new char[2 * str.length() + 1];
        int index = 0;
        for (int i = 0; i != manacherStr.length; i++) {
            manacherStr[i] = (i & 1) == 0? '#': chars[index++];
        }
        System.out.println(Arrays.toString(manacherStr));

        return manacherStr;
    }

    public int manacher(String str) {
        if (str == null || str.length() == 0) {
            return 0;
        }

        char[] charArr = manacherString(str);
        int[] pArr = new int[charArr.length];
        int C = -1;
        int R = -1;
        int max = Integer.MIN_VALUE;
        int earlyC = -1;

        for (int i = 0; i < charArr.length; i++) {
            pArr[i] = i < R? Math.min(R - i, pArr[2*C-i]): 1;
            while (i + pArr[i] < charArr.length && i - pArr[i] > -1) {
                if (charArr[i + pArr[i]] == charArr[i - pArr[i]]) {
                    pArr[i]++;
                }else {
                    break;
                }
            }
            if (i + pArr[i] > R) {
                R = i + pArr[i];
                C = i;
            }
            if (R == charArr.length) {
                earlyC = pArr[i];
                break;
            }
            max = Math.max(max, pArr[i]);
        }
        System.out.println(earlyC);

//        方式1
//        char[] res = new char[str.length()-earlyC+1];
//        for (int i = 0; i < res.length; i++) {
//            res[res.length-1-i] = charArr[i*2+1];
//        }

        // 方式2
        char[] res = new char[(2*C-charArr.length+1)/2];
        int index = 0;
        for (int i = 2*C-charArr.length; i >=0; i--) {
            if ((i & 1) != 0) {
                res[index++] = charArr[i];
            }
        }
        System.out.println(String.valueOf(res) + " " + res.length);
        return max;
    }

    public static void main(String[] args) {
        new Manacher().manacher("abcxyzfzyx");
    }
}

结果:
cba 3
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值