leetcode5:最长回文子串

来源:互联网 发布:mac下制作黑苹果u盘 编辑:程序博客网 时间:2024/06/05 20:15

  这个题目主要是运用manacher算法,又称为马拉车算法,下面对算法进行介绍。

manacher算法

  首先,定义一些变量,假设当前访问的是第i个位置:
    1.p[i]:表示以第i个字符为中心的最长回文子串的半径。
    2.maxRight:i之前(包括i)所对应的回文子串所能到达的最右边的位置。

  然后假设现在要求以第j(j=i+k)个字符为中心的最长回文串的半径p[j],假设在j之前,是以第i个字符为中心的最长回文串能够到达最右边的位置,即位置maxRight,则说明maxRight=i+p[i]。要快速的求出以第j个字符为中心的最长回文子串半径,一定要利用好之前已经求得的回文子串。对j的位置进行讨论:
  1.j>=maxRight:由于maxRight=i+p[i],所以j所处的位置不在第i个字符为中心的最长回文子串中,无法利用以前求得的经验,这是得用一般的方法来求p[j],即初始化p[j]=1,因为此时只包含本身。然后依次判断左右两边的字符是否相等,来确定p[j]的最长子串半径大小。

  2.j<maxRight:由于maxRight=i+p[i],所以j所处的位置在第i个字符为中心的最长回文子串中。此时,我们可以利用回文串的称关系来求解。因为已经假设:j=i+k,所以求p[j]p[i+k]时,可以利用其关于i的对称点ik所对应的p[ik]值来求解。此时需要对(ikp[ik])(ip[i])的大小分三种情况来讨论。

  2.1:(ikp[ik])>(ip[i]):如图所示

这里写图片描述

此时p[ik]对应的最长回文串被p[i]对应的最长回文串的左半部分完全包围,根据回文的对称性,p[i+k]也应该被右半部分完全包围,并且p[ik]=p[i+k]=p[j],很好理解,不需要证明。

  2.2:(ikp[ik])=(ip[i]):如图所示

这里写图片描述

此时p[ik]对应的最长回文串还是被p[i]对应的最长回文串所包围,所以对应的p[i+k]的最长回文串至少也会到达i+p[i]的位置,即maxRight处,但是因为maxRight位置之后的字符情况未知,可能还会有与以i+k为中心的左边位置的字符相等,所以p[i+k]的值可能还会更长,此时需要再次比较ch[left]==ch[right]的情况来判断是否会更长,此时p[i+k]>=p[ik],应该从(i+kp[ik])(i+k+p[ik])两个位置进行左右比较。

  2.3:(ikp[ik])=(ip[i]):如图所示

这里写图片描述

此时,p[i]对应的最长回文子串的左半部分无法完整的包含p[ik]对应的最长回文子串,此时p[i+k]=p[i]k,即最长回文子串半径仅仅包含ip[i]ik之间的那一小段。能不能更长呢,答案是否定的,下面给予证明。
  假设p[i+k]的值能够更大,即p[i+k]>p[i]k,如图所示,这相当于在(i+p[i])的位置后面延伸了一小段(图中最右边的那段小粗横线),根据根据对称性,图中另外几段小粗黑线必须也有的。假设从左到右依次标注那几段小黑线为L1,L2,L3,L4,现在假设L4是存在的,假设其值为ABC,则根据回文的对称性,L3的值为CBAL2的值为ABCL1的值为CBA,可以发现L1L4也是回文对称的,所以以位置i为中心的最长回文串应该更长,不应该是p[i],而应该是p[i]+3,因为ABC的长度为3,但是这明显最原始的假设不符合,因为最原始假设就是为p[i]。所以p[i+k]只能等于p[i]k

  然后对上述三种情况进行汇总:

p[i+k]=p[ik],p[ik],p[i]k,p[i]-k>p[i-k]p[i]-k=p[i-k]p[i]-k<p[i-k]

观察总结之后发现,p[i+k]=min{p[ik],p[i]k},当然,对于第二种情况,即可能更长的情况,其实可能会更大,我们将上面三种也统一,在赋值完之后,在进行左右比较,因为对于第一种和第三种情况,进行左右比较时肯定不相等,不影响值的变化,对于第二种情况进行左右比较就可以得到真实最长半径。

2.字符串预处理

  在求最长回文子串的时候,我们还得对区分以单字符为中心的最长回文子串和以双字符为中心的最长回文子串进行判断,为了便于处理,我们可以通过在字符串中加入统一的字符,将奇偶情况统一为以单字符为中心的最长子串。例如:对于字符串“abbc”,我们可以通过加入’#’字符,将其变为”#a#b#b#c#”,这样就变为以单字符为中心;对于字符”aba”,也通过加入’#’,将其变为”#a#b#a#”,仍然是以单字符为中心。就完成了统一,也便于使用manacher算法,因为该算法就是以单字符为中心。
  但是由于最左边和最右边都是’#’,这样在进行左右判断的时候,就会leftright++成立,会出现越界的情况。为了避免这种越界的发生,我们可以在两端再次添加两个不同的字符,例如最左边添加’@’,最右边添加’=’,此时”aba”处理之后的结果为”@#a#b#a#=”。

3.代码实现

    public static String solve(String ss) {        /*         * 加#号,使得两种情况统一处理         * 如"aa"变为"#a#a#"         * 再如"aba"变为"#a#b#a#"         * 防止越界,在首部再加上@,尾部加上$,只要不同即可,防止越界         */        char[] cs = ss.toCharArray();        int len = cs.length * 2 + 3;                     //首部,尾部加一个不同的字符,防止越界        char[] res = new char[len];        res[0] = '@';        res[len - 1] = '$';        for(int i = 1; i < len - 1; i++) {            if((i&1) != 0) {                            //如果是奇数                res[i] = '#';            }            else {                                      //偶数                res[i] = cs[i / 2 - 1];            }        }        int maxRight = 0;                               //能到的最右边                   int longestCenter = 0;                          //最长的中心        int longest = 0;                                //最长        int nowCenter = 0;                              //到达最右边对应的中心        int[] dp = new int[len];                        //以i位置就中心的最长回文串        for(int i = 1; i < len - 1; i++) {            if(maxRight > i) {                dp[i] = Math.min(dp[2*nowCenter - i], maxRight - i);            }            else {                dp[i] = 1;            }            //考虑以此为中心,继续递增,因为恰好在边界上需要考虑            while(res[i + dp[i]] == res[i - dp[i]]) {                dp[i]++;            }            if(dp[i] + i > maxRight) {                maxRight = dp[i] + i;                nowCenter = i;            }            if(longest < dp[i]) {                longest = dp[i];                longestCenter = i;            }        }        StringBuffer sb = new StringBuffer();        for(int i = longestCenter - longest + 1; i < longestCenter + longest - 1; i++) {            if(res[i] != '#')                sb.append(res[i]);        }        return sb.toString();    }
原创粉丝点击