Manacher算法的图示理解和代码实现

Manacher算法的图示理解和代码实现

需要解决的问题

寻找一个字符串中最长的回文子串。

第一种:暴力解法

思想:以i为起点,向两边扩展,寻找以i为中心的最长回文子串。

复杂度:O(n2)

补充:回文的形式有奇数回文和偶数回文,比如1221是偶数回文,12321是奇数回文。上面的思想只能解决奇数回文的情况,解决方法是给字符串的首尾和中间加上辅助字符#,比如1221变成#1#2#2#1#12321变成#1#2#3#2#1#,这样做的好处是将偶数回文和奇数回文统一起来,都可以用上面的思想来求解。

第二种:Manacher算法求解

复杂度:O(n)

准备工作:也需要将偶数回文和奇数回文统一起来,即加上辅助字符。

Manacher算法中的三个概念:

1、【回文直径和回文半径】:以一个中心位置i,向两边扩展,得到的回文串的长度叫做该位置的回文直径,回文串一半的长度叫做该位置的回文半径。进一步说明,因为加上辅助字符之后得到的回文字符串的长度一定是奇数,所以回文半径是包括回文中心在内的回文子串的一半的长度,回文直径则是回文半径的2倍减1。比如对于字符串 "aba",在字符 'b'处的回文半径就是2,回文直径就是3。

将每个位置上的回文半径存在一个数组中保存下来,这个数组叫做【回文半径数组】,在求解i位置上的回文半径的时候就可以利用前面已经求得的回文半径数组进行计算加速。

2、【回文右边界】:所有回文半径中最右边的边界。注意:回文右边界是回文串右边的一个位置,即这个位置不在回文串内。下面举例说明:

解释:上面的箭头表示回文右边界,下面的箭头表示从左往右依次访问的位置,当index=0时,回文串为本身“0”,所以回文右边界位于字符串"0"的右边,即上面第一个箭头,当index=1时,回文串为本身“1”,此时回文右边界是上面的第二个箭头;当index=2时,“121”构成回文串,所以回文右边界位于上面的第三个箭头即字符‘3’所在的位置;当index=4时,整个字符串构成回文串,所以回文右边界位于index=9的位置。

3、【回文右边界中心】:回文右边界对应的中心位置。对于上面的字符串,当回文右边界在index=9位置上时,中心在index=4这个位置上,并且是在这个位置上最早得到的回文右边界。什么叫最早得到的回文右边界?因为对于字符‘3’之后的字符串121,回文右边界也都在最后一个位置,但是字符‘3’这个位置是最早由该位置得到回文右边界的。

Manacher算法中分四种情况讨论

1、当前位置i在回文右边界的外边,方法:暴力破解。

第2、3、4种情况是基于当前位置i在回文右边界的里面。如果当前位置i在回文右边界的里面(左边),如下图:

解释:图中R表示回文右边界,C表示回文右边界中心,LR的范围是回文直径,当前位置i关于C的镜像是i',根据回文半径数组可以确定i'位置上的回文范围,即图中用中括号圈出来的部分。

2、i'位置上的回文范围在(L, R)内,此时i位置的回文半径和i'位置的回文半径相等。
3、i'位置上的回文范围不完全在(L, R)内,此时i位置的回文半径为R-i
4、i'位置上的回文范围左边界和L重合,方法:暴力破解。

示例如下:

Manacher算法的代码实现

public class Manacher {
    /**
     * 对输入字符串预处理,加上辅助字符#
     * @param str
     * @return
     */
    private static char[] preprocess(String str) {
        char[] strArr = new char[str.length()*2+1];
        for (int i = 0; i < strArr.length; i++) {
            strArr[i] = (i&1) == 0 ? '#' : str.charAt(i/2);
        }
        return strArr;
    }

    /**
     * 寻找一个字符串中最长的回文子串,返回这个回文字符串的长度
     * @param str
     * @return
     */
    public static int maxLcpsLength(String str) {
        if (str == null || str.length() == 0) {
            return 0;
        }
        char[] strArr = preprocess(str);
        int[] rArr = new int[strArr.length]; // 回文半径数组
        int R = -1; // 回文右边界
        int C = -1; // 回文中心
        int max = Integer.MIN_VALUE;
        for (int i = 0; i < strArr.length; i++) {
            // 确定i位置回文半径的初始值
            // 当i>=R时,最短的回文半径是1,也就是字符本身;反之,最短的回文半径可能是i对应的i'的回文半径或者i到R的距离
            rArr[i] = R > i ? Math.min(rArr[2*C-i], R-i) : 1;
            // 尝试向外扩展
            while (i + rArr[i] < strArr.length && i - rArr[i] >= 0) {
                if (strArr[i+rArr[i]] == strArr[i-rArr[i]]) {
                    rArr[i]++;
                } else {
                    break;
                }
            }
            if (i + rArr[i] > R) {
                R = i+rArr[i];
                C = i;
            }
            // 更新最大回文半径的值
            max = Math.max(max, rArr[i]);
        }
        // 这里为什么返回值是max-1,因为预处理之后的字符串和原字符串不同,
        // 所以这里得到的最大回文半径其实是原字符串的最大回文子串长度加1
        return max - 1;
    }

    public static void main(String[] args) {
        String str1 = "12321";
        System.out.println(maxLcpsLength(str1));
    }
}

Manacher算法的应用

在一个字符串的最后添加一个字符串,使得添加之后的整体字符串是一个回文串,要求添加的字符串长度最短。例如:abc12321,则需要在最后添加上cba,使得abc12321cba变成一个回文串,也可以添加整体的逆序12321cba,添加之后整体也是回文串,但是cba是最短的。

参考资料:
左神算法课程
Manacher算法详解

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值