Manacher's ALGORITHM: O(n)时间求字符串的最长回文子串

资料来源网络 参见:http://www.felix021.com/blog/read.php?2040

求解字符串的回文问题的时候,如果不对字符串做一些处理,我们会遇到回文子串的长度是奇数或者偶数的分类处理。

但是,采用Manacher算法可以完全避免这个问题。

Manacher算法:在原来字符串的头、尾以及字符之间都添加一个从来没有出现过的字符,作为分隔符。例如:”#”。
就可以把奇数和偶数区别问题完全转化为奇数问题。

具体做法:在每个字符的两边都插入一个特殊的符号。
比如 abba 变成 #a#b#b#a#, aba变成 #a#b#a#。 由于我采用java语言,就免去避免越界的额外添加操作。

下面以字符串12212321为例,经过上一步,变成了 S[] = “$#1#2#2#1#2#3#2#1#”;
用一个数组 P[i] 来记录以字符S[i]为中心的最长回文子串向左/右扩张的长度(包括S[i],也就是把该回文串“对折”以后的长度),比如S和P的对应关系:

S  #  1  #  2  #  2  #  1  #  2  #  3  #  2  #  1  #
P  1  2  1  2  5  2  1  4  1  2  1  6  1  2  1  2  1

由于L=2*P[i] -1,是新字符串中以S[i]为中心的回文子串的最长长度;
新字符串都是以”#”开始或者结束,原字符串的回文长度为:(L-1)/2;
由上面两个式子可以得到原字符串回文长度为:P[i]-1。

现在的问题是:如何求解P[i]数组的值?

该算法增加两个辅助变量(其实一个就够了,两个更清晰)id和mx,其中 id 为已知的 {右边界最大} 的回文子串的中心,mx则为id+P[id],也就是这个子串的右边界。

然后可以得到一个非常神奇的结论,这个算法的关键点就在这里了:如果mx > i,那么P[i] >= MIN(P[2 * id - i], mx - i)。就是这个串卡了我非常久。实际上如果把它写得复杂一点,理解起来会简单很多:

//记j = 2 * id - i,也就是说 j 是 i 关于 id 的对称点(j = id - (i - id))
if (mx - i > P[j]) 
    P[i] = P[j];
else /* P[j] >= mx - i */
    P[i] = mx - i; // P[i] >= mx - i,取最小值,之后再匹配更新

当然光看代码还是不够清晰,还是借助图来理解比较容易。

当 mx - i > P[j] 的时候,以S[j]为中心的回文子串包含在以S[id]为中心的回文子串中,由于 i 和 j 对称,以S[i]为中心的回文子串必然包含在以S[id]为中心的回文子串中,所以必有 P[i] = P[j],见下图。
这里写图片描述

当 P[j] >= mx - i 的时候,以S[j]为中心的回文子串不一定完全包含于以S[id]为中心的回文子串中,但是基于对称性可知,下图中两个绿框所包围的部分是相同的,也就是说以S[i]为中心的回文子串,其向右至少会扩张到mx的位置,也就是说 P[i] >= mx - i。至于mx之后的部分是否对称,就只能老老实实去匹配了。
这里写图片描述

对于 mx <= i 的情况,无法对 P[i]做更多的假设,只能P[i] = 1,然后再去匹配了。

反思
个人在这里卡了很长时间,并且刚刚开始都不小这句话是来干嘛的。
如果mx > i,那么P[i] >= MIN(P[2 * id - i], mx - i)。

主要是经常做某公司的题目,动不动就用dp,所以一开始用的dp做的,但是提交以后表示:超时无解。
代码如下:

import java.util.Scanner;

/**
 * 动态规划问题
 * 
 * @author luopan
 *
 */
public class Mian {

    /**
     * 返回有效密码串的最大长度
     * 
     * 求解最长对称子串
     * 
     * 求最长回文
     * 
     * @param str
     * @return
     */
    private static int getMaxLength(String str) {

        int len = str.length();

        int maxLen = 0;

        // 1、声明dp二维数组记录[i-j]之间是不是回文
        int dp[][] = new int[len][len];

        // 2、初始化dp数组,并且每个字符串本身都是回文,设置为1,表示是
        for (int i = 0; i < len; i++) {
            dp[i][i] = 1;
        }

        // 3、初始化dp数组,并且相连的两个字符串相等的也是回文,设置为1,表示是
        for (int i = 0; i < len - 1; i++) {
            if (str.charAt(i) == str.charAt(i + 1)) {
                dp[i][i + 1] = 1;
            }
        }

        // 4、计算以某个字符为中心,左右发散比较是否相等
        // 外层循环l表示包括回文中心的总长度,从只有一个左和一个右开始循环,所以是3
        for (int l = 3; l <= len; l++) {
            for (int i = 0; i <= len - l; i++) {
                int j = i + l - 1;
                if (str.charAt(i) == str.charAt(j)) {
                    // 同时向左向右发散
                    // i在i+1的左边
                    // j在j-1的右边
                    dp[i][j] = dp[i + 1][j - 1];
                    if (dp[i][j] == 1 && l > maxLen) {
                        maxLen = (j + 1) - i;
                        // String longStr = str.substring(i, j+1);
                    }
                } else {
                    dp[i][j] = 0;
                }
            }
        }

        return maxLen;
    }

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()) {
            String str = scanner.nextLine().trim();
            System.out.println(getMaxLength(str));
        }
        scanner.close();
    }

}

对比着看了一下,Manacher算法保存了之前计算好的值,而我自己写的dp没有保存,每次都要重新计算,严重加长了运行时间。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值