最长回文子串——Manacher 算法

0. 问题定义

最长回文子串问题:给定一个字符串,求它的最长回文子串长度。java

若是一个字符串正着读和反着读是同样的,那它就是回文串。下面是一些回文串的实例:python

12321    a    aba    abba    aaaa   tattarrattat(牛津英语词典中最长的回文单词)

1. Brute-force 解法

对于最长回文子串问题,最简单粗暴的办法是:找到字符串的全部子串,遍历每个子串以验证它们是否为回文串。一个子串由子串的起点和终点肯定,所以对于一个长度为n的字符串,共有n^2个子串。这些子串的平均长度大约是n/2,所以这个解法的时间复杂度是O(n^3)。算法

2. 改进的方法

显然全部的回文串都是对称的。长度为奇数回文串以最中间字符的位置为对称轴左右对称,而长度为偶数的回文串的对称轴在中间两个字符之间的空隙。能否利用这种对称性来提升算法效率呢?答案是确定的。咱们知道整个字符串中的全部字符,以及字符间的空隙,均可能是某个回文子串的对称轴位置。能够遍历这些位置,在每一个位置上同时向左和向右扩展,直到左右两边的字符不一样,或者达到边界。对于一个长度为n的字符串,这样的位置一共有n+n-1=2n-1个,在每一个位置上平均大约要进行n/4次字符比较,因而此算法的时间复杂度是O(n^2)。数组

3. Manacher 算法

对于一个比较长的字符串,O(n^2)的时间复杂度是难以接受的。Can we do better? spa

先来看看解法2存在的缺陷。翻译

1) 因为回文串长度的奇偶性形成了不一样性质的对称轴位置,解法2要对两种状况分别处理;
2) 不少子串被重复屡次访问,形成较差的时间效率。code

缺陷2)能够经过这个直观的小?体现:rem

char: a b a b a
  i : 0 1 2 3 4

当i==1,和i==2时,左边的子串aba分别被遍历了一次。字符串

若是咱们能改善解法2的不足,就颇有但愿能提升算法的效率。Manacher正是针对这些问题改进算法。it

(1) 解决长度奇偶性带来的对称轴位置问题

Manacher算法首先对字符串作一个预处理,在全部的空隙位置(包括首尾)插入一样的符号,要求这个符号是不会在原串中出现的。这样会使得全部的串都是奇数长度的。以插入#号为例:

aba  ———>  #a#b#a#
abba ———>  #a#b#b#a#

插入的是一样的符号,且符号不存在于原串,所以子串的回文性不受影响,原来是回文的串,插完以后仍是回文的,原来不是回文的,依然不会是回文。

(2) 解决重复访问的问题

咱们把一个回文串中最左或最右位置的字符与其对称轴的距离称为回文半径。Manacher定义了一个回文半径数组RL,用RL[i]表示以第i个字符为对称轴的回文串的回文半径。咱们通常对字符串从左往右处理,所以这里定义RL[i]为第i个字符为对称轴的回文串的最右一个字符与字符i的距离。对于上面插入分隔符以后的两个串,能够获得RL数组:

char:    # a # b # a #
 RL :    1 2 1 4 1 2 1
RL-1:    0 1 0 3 0 1 0
  i :    0 1 2 3 4 5 6

char:    # a # b # b # a #
 RL :    1 2 1 2 5 2 1 2 1
RL-1:    0 1 0 1 4 1 0 1 0
  i :    0 1 2 3 4 5 6 7 8

上面咱们还求了一下RL[i]-1。经过观察能够发现,RL[i]-1的值,正是在本来那个没有插入过度隔符的串中,以位置i为对称轴的最长回文串的长度。那么只要咱们求出了RL数组,就能获得最长回文子串的长度。

因而问题变成了,怎样高效地求的RL数组。基本思路是利用回文串的对称性,扩展回文串

咱们再引入一个辅助变量MaxRight,表示当前访问到的全部回文子串,所能触及的最右一个字符的位置。另外还要记录下MaxRight对应的回文串的对称轴所在的位置,记为pos,它们的位置关系以下。

咱们从左往右地访问字符串来求RL,假设当前访问到的位置为i,即要求RL[i],在对应上图,i必然是在po右边的(obviously)。但咱们更关注的是,i是在MaxRight的左边仍是右边。咱们分状况来讨论。

1)当iMaxRight的左边

状况1)能够用下图来刻画:

咱们知道,图中两个红色块之间(包括红色块)的串是回文的;而且以i为对称轴的回文串,是与红色块间的回文串有所重叠的。咱们找到i关于pos的对称位置j,这个j对应的RL[j]咱们是已经算过的。根据回文串的对称性,以i为对称轴的回文串和以j为对称轴的回文串,有一部分是相同的。这里又有两种细分的状况。

  1. j为对称轴的回文串比较短,短到像下图这样。

这时咱们知道RL[i]至少不会小于RL[j],而且已经知道了部分的以i为中心的回文串,因而能够令RL[i]=RL[j]。可是以i为对称轴的回文串可能实际上更长,所以咱们试着以i为对称轴,继续往左右两边扩展,直到左右两边字符不一样,或者到达边界。

  1. j为对称轴的回文串很长,这么长:

这时,咱们只能肯定,两条蓝线之间的部分(即不超过MaxRight的部分)是回文的,因而从这个长度开始,尝试以i为中心向左右两边扩展,,直到左右两边字符不一样,或者到达边界。

不论以上哪一种状况,以后都要尝试更新MaxRightpos,由于有可能获得更大的MaxRight。

具体操做以下:

step 1: 令RL[i]=min(RL[2*pos-i], MaxRight-i)
step 2: 以i为中心扩展回文串,直到左右两边字符不一样,或者到达边界。
step 3: 更新MaxRight和pos

2)当iMaxRight的右边

遇到这种状况,说明以i为对称轴的回文串尚未任何一个部分被访问过,因而只能从i的左右两边开始尝试扩展了,当左右两边字符不一样,或者到达字符串边界时中止。而后更新MaxRightpos

(3) 算法实现

#Python
def manacher(s):
    #预处理
    s='#'+'#'.join(s)+'#'

    RL=[0]*len(s)
    MaxRight=0
    pos=0
    MaxLen=0
    for i in range(len(s)):
        if i<MaxRight:
            RL[i]=min(RL[2*pos-i], MaxRight-i)
        else:
            RL[i]=1
        #尝试扩展,注意处理边界
        while i-RL[i]>=0 and i+RL[i]<len(s) and s[i-RL[i]]==s[i+RL[i]]:
            RL[i]+=1
        #更新MaxRight,pos
        if RL[i]+i-1>MaxRight:
            MaxRight=RL[i]+i-1
            pos=i
        #更新最长回文串的长度
        MaxLen=max(MaxLen, RL[i])
    return MaxLen-1

(4) 复杂度分析

空间复杂度:插入分隔符造成新串,占用了线性的空间大小;RL数组也占用线性大小的空间,所以空间复杂度是线性的。
时间复杂度:尽管代码里面有两层循环,经过amortized analysis咱们能够得出,Manacher的时间复杂度是线性的。因为内层的循环只对还没有匹配的部分进行,所以对于每个字符而言,只会进行一次,所以时间复杂度是O(n)

4. 更多关于回文串的 fun facts(参考自维基百科)

4.1 人们在一座名为赫库兰尼姆的古城遗迹中,找到了一个好玩的拉丁语回文串:sator arepo tenet opera rotas。翻译成中文大概就是`一个叫作Arepo的播种者,他用力地扶(把)着车轮。这个串的每一个单词首字母恰好组成了第一个单词,每一个单词的第二个字母恰好组成了第二个单词...因而乎,若是写出酱紫,你会发现上下左右四个方向读起来是同样的。这个串被称为 Sator Square.

4.2 本文开头给出的单词tattarrattat,出如今爱尔兰做家詹姆斯·乔伊斯的小说《尤利西斯》,是敲门的意思。吉尼斯纪录的最长回文英文单词是detartrated,是个化学术语。另外,还有些已出版的英文回文小说(大家歪果仁真会玩),好比Satire: VeritasDr Awkward & Olson in Oslo等。

2015.11.9 更新。

能够采用动态规划,列举回文串的起点或者终点来解最长回文串问题,无需讨论串长度的奇偶性。
看下面的扎瓦代码,容易理解。

public int longestPalindrome(String s) {
     int n=s.length();
     boolean[][] pal=new boolean[n][n];
     //pal[i][j] 表示s[i...j]是不是回文串
     int maxLen=0;
     for (int i=0;i<n;i++){  // i做为终点
         int j=i;    //j做为起点
         while (j>=0){
             if (s.charAt(j)==s.charAt(i)&&(i-j<2||pal[j+1][i-1])){
                 pal[j][i]=true;
                maxLen=Math.max(maxLen, i-j+1);
             }
             j--;
         }
     }
     return maxLen;
    }
相关文章
相关标签/搜索