Manacher's algorithm

Manacher算法主要解决的问题是求给定字符串中最长的回文字符串。

以前咱们求解回文字符串的步骤是找中心点, 然后根据中心点向左右expand,这种做法有两个缺点,一个是我们要兼容中心点奇/偶的问题,另一个是算法时间复杂度为O(n2),效率不咋地。

Manacher算法很好的解决了这两个问题,把给定字符串改成奇数长度,并且算法的复杂性为O(n)。

首先该算法通过在给出字符串每个字符之间加入'#'号使得无论给出的字符串是奇数长度还是偶数长度都会同意转为奇数长度。

比如给出的字符串是'ab',转换后变成'#a#b#',在考虑边界问题,因为回文问题通常需要一个left/right指针,为了使算法不用去兼容数组越界的异常,我们可以再在字符串首尾再分别加上$和@,可以得到'$#a#b#@',为什么是$和@,其实只要和给定字符串中的字母不重复就好了。

https://www.jianshu.com/p/4b71e71cfb30 此题的Manacher算法实现

    public int countSubstrings(String S) {
        /**
         * 第一步字符之间加入#,首尾加入@/$
         */
        char[] A = new char[2 * S.length() + 3];
        A[0] = '@';
        A[1] = '#';
        A[A.length - 1] = '$';
        int t = 2;
        for (char c: S.toCharArray()) {
            A[t++] = c;
            A[t++] = '#';
        }
        /**
         * 初始化Z[],存储A[]对应字符为中心的回文半径(我们定义回文半径如下,如aba,在中心为b的情况下半径为1)
         */
        int[] Z = new int[A.length];
        /**
         * 定义已知最长回文中心点center和对应的半径
         */
        int center = 0, right = 0;
        /**
         * 起始末尾的@/$是为了兼容数组越界异常,所以这里i从1开始并在Z.length-2结束
         */
        for (int i = 1; i < Z.length - 1; ++i) {
            /**
             * 这一步是Manacher算法的精髓
             * 我们假设S[a]的意思是以a为中心点的回文字符串
             * 当i小于right时表示以i为中心组成的回文至少有一部分是包含在以center为中心点的回文中
             * 情况如图 left--(2 * center - i)---center---i--right
             * 并且S[i]包含在S[center]的部分和S[2 * center - i]是一样的
             * 我们可以通过简单的方法提前知道以i为中心的回文的长度范围
             *
             * 2 * center - i表示i关于center对称的元素,由center-j=i-center可以推导出
             * j-left起始就是Z[2 * center - i]
             * 那么现在有两种情况,如果right-i > j-left,说明 S[j]完全包含在S[center]中
             * 那么作为j的映射点i的回文和S[j]是一摸一样的,可以推导出Z[i] = Z[j]
             * 如果right-i <= j-left 说明 S[j]部分包含在S[center]中
             * 那么作为j的映射点i的回文和S[j]包含在S[center]中的部分是一摸一样的
             * 对于当i>=right的部分则需要利用后面的while循环继续判断,这样我们可以推导出Z[i] >= right-i
             * 这里我们把Z[i]设置成最小值right-i
             * Manacher算法就是利用包含在S[center]中对称节点的一致性做出了如上推导,使之后的while循环更加高效
             */
            if (i < right){
                Z[i] = Math.min(right - i, Z[2 * center - i]);
            }
            /**
             * 这一步就是那个假设,如过S[i,j]是回文,当S[i-1]=S[j+1]时S[i-1,j-1]也是回文
             */
            while (A[i + Z[i] + 1] == A[i - Z[i] - 1]){
                /**
                 * 对应回文半径+1
                 */
                Z[i]++;
            }
            /**
             * 当前最长半径>已知最长半径
             * 更新中心点和已知最长半径
             */
            if (i + Z[i] > right) {
                center = i;
                right = i + Z[i];
            }
        }
        int ans = 0;
        for (int v: Z) {
            /**
             * 1.为什么要v+1?
             * 考虑一个中心点为C半径为R的回文共有多少种回文组合,答案是R-1,R-2,R-3...0,一共R+1中组合
             * 2.为什么要除以2
             * 以为我们把给定字符串中插入了#,使得原字符长度扩大了两倍
             */
            ans += (v + 1) / 2;
        }
        /**
         * 为什么Manacher算法时间复杂度是O(n)?明明有两个for循环
         * 考虑第二个while循环只在Z[i] >= right-i时才会运行多次,并且运行的次数是有上限的
         * ,因为每次执行while循环都会导致Z[i]++,而right = i + Z[i]
         * 并且right<=2S + 1(因为right最大为i,而i最大=Z.length - 2 = 2 * S.length() + 3 - 2)
         */
        return ans;
    }

你可能感兴趣的:(Manacher's algorithm)