LeetCode005__Longest Palindromic Substring

来源:互联网 发布:淘宝账期延长怎么解除 编辑:程序博客网 时间:2024/06/05 21:04

1、引言

还是国际惯例,先贴上leetcode上的原题:

Given a string S, find the longest palindromic substring in S. You may assume that the maximum length of S is 1000, and there exists one unique longest palindromic substring.

这是一道关于字符串的题目,找寻字符串中的最大回文子串,这让我回想到考研时候数据结构中最让我头疼的KMP算法,真好趁这道题目的机会重新温故一下字符串的应用,主要以字符串的匹配,以及最大回文串,最大回文子序列三个方面来实现。

因为回顾比较多,所以本文有些长,并且部分脱离了Leetcode范畴。

2、KMP算法

kmp算法又称“看毛片”算法,是一个效率非常高的字符串匹配算法。不过由于其难以理解,所以在很长的一段时间内一直没有搞懂。虽然网上有很多资料,但是鲜见好的博客能简单明了地将其讲清楚。在此,综合网上比较好的几个博客(参见最后),尽自己的努力争取将kmp算法思想和实现讲清楚。

kmp算法完成的任务是:给定两个字符串O和f,长度分别为n和m,判断f是否在O中出现,如果出现则返回出现的位置。常规方法是遍历a的每一个位置,然后从该位置开始和b进行匹配,但是这种方法的复杂度是O(nm)。kmp算法通过一个O(m)的预处理,使匹配的复杂度降为O(n+m)。(以上来源http://blog.csdn.net/yutianzuijin/article/details/11954939/)

在之前阅读相关博客和资料的时候,我一直不太明白,怎么确保匹配子串发生移动时,在最大小标情况下保证之前的元素与被匹配数组元素相同。

以下将对此进行解释,但愿自己能够说清楚吧。(实例来源http://www.matrix67.com/blog/archives/115)

假如,A=”abababaababacb”,B=”ababacb”,我们来看看KMP是怎么工作的。我们用两个指针i和j分别表示,A[i-j+ 1..i]与B[1..j]完全相等。也就是说,i是不断增加的,随着i的增加j相应地变化,且j满足以A[i]结尾的长度为j的字符串正好匹配B串的前 j个字符(j当然越大越好),现在需要检验A[i+1]和B[j+1]的关系。当A[i+1]=B[j+1]时,i和j各加一;什么时候j=m了,我们就说B是A的子串(B串已经整完了),并且可以根据这时的i值算出匹配的位置。当A[i+1]<>B[j+1],KMP的策略是调整j的位置(减小j值)使得A[i-j+1..i]与B[1..j]保持匹配且新的B[j+1]恰好与A[i+1]匹配(从而使得i和j能继续增加)。我们看一看当 i=j=5时的情况。

i = 1 2 3 4 5 6 7 8 9 ……A = a b a b a b a a b a b …B = a b a b a c bj = 1 2 3 4 5 6 7

第一次比对到i==5,因为i=6时候发现A与B元素不相同,故而开始进行回头重比较:

i = 1 2 3 4 5 6 7 8 9 ……A = a b a b a b a a b a b …B =     a b a b a c bj =     1 2 3 4 5 6 7

这样问题来了,KMP算法采用什么机制或者说具体是如何进行比对保证j==4的时候B[5]前面的元素与A[7]之前的四个元素相同呢?

其实道理很简单,我们可以看到,新的j可以取多少与i无关,只与B串有关。我们完全可以预处理出这样一个数组P[ j ],表示当匹配到B数组的第j个字母而第j+1个字母不能匹配了时,新的j最大是多少。P[j]应该是所有满足B[ 1…P[ j ] ]=B[ j-P[ j ]+1 … j ]的最大值。

因此,代码可以理解为扫描字符串A,并更新可以匹配到B的什么位置。

于是,我们需要一个算法实现P数组的预处理过程。

伪代码如下:

p[1] <- 0;j <- 0;for i <- 2 to m do{    while j > 0 and B[j+1] ≠ B[i] do j <- P[j];    if B[j+1] = B[i] then j <- j+1;    p[i] <- j;}

其实代码很简单,实现了对于B数组的一次自我匹配,但是其中用到了摊还思想,需要细细品味。

有了P数组,那么得到KMP整体算法就没那么难了,

j <- 0;for i <- 1 to m do{    while j > 0 and B[j+1] ≠ A[i] do j <- P[j];    if B[j+1] = A[i] then j <- j+1;    if j = m then  writeln('Pattern occurs with shift ',i-m);    p[i] <- j;}

附上python代码:

class KMP(object):    def kmpAlgorithmPre(self, a):        p=[0]*len(a)        i = 0        for j in range (1,len(a)-1):            while (i>0 and a[i]!=a[j]):                i = p[i]               # print(i)            if a[i] == a[j]:                i = i+1            p[j] = i        print(p)        return p    def kmpAlorithm(self,a,b):        pre = self.kmpAlgorithmPre(b)        m = -1        samesub=[]        temp = []        for n in range(0,len(a)-1):            while(m>0 and b[m+1]!=a[n]):                temp = []                #print(m)                m = pre[m]            if b[m+1] == a[n]:                m = m+1                temp.append(b[m])            if len(temp)>len(samesub):                samesub = temp[:]            if m ==len(b)-1:                m = pre[m]        return samesub

3、Manacher算法

此部分参考自:http://blog.csdn.net/dyx404514/article/details/42061017
下面介绍Manacher算法的原理与步骤。
首先,Manacher算法提供了一种巧妙地办法,将长度为奇数的回文串和长度为偶数的回文串一起考虑,具体做法是,在原字符串的每个相邻两个字符中间插入一个分隔符,同时在首尾也要添加一个分隔符,分隔符的要求是不在原串中出现,一般情况下可以用#号。下面举一个例子:

(1)Len数组简介与性质

Manacher算法用一个辅助数组Len[i]表示以字符T[i]为中心的最长回文字串的最右字符到T[i]的长度,比如以T[i]为中心的最长回文字串是T[l,r],那么Len[i]=r-i+1。
对于上面的例子,可以得出Len[i]数组为:

Len数组有一个性质,那就是Len[i]-1就是该回文子串在原字符串S中的长度,至于证明,首先在转换得到的字符串T中,所有的回文字串的长度都为奇数,那么对于以T[i]为中心的最长回文字串,其长度就为2*Len[i]-1,经过观察可知,T中所有的回文子串,其中分隔符的数量一定比其他字符的数量多1,也就是有Len[i]个分隔符,剩下Len[i]-1个字符来自原字符串,所以该回文串在原字符串中的长度就为Len[i]-1。
有了这个性质,那么原问题就转化为求所有的Len[i]。下面介绍如何在线性时间复杂度内求出所有的Len。

(2)Len数组的计算

首先从左往右依次计算Len[i],当计算Len[i]时,Len[j](0<<=j<< i)已经计算完毕。设P为之前计算中最长回文子串的右端点的最大值,并且设取得这个最大值的位置为po,分两种情况:
第一种情况:i<=P
那么找到i相对于po的对称位置,设为j,那么如果Len[j]<< P-i,如下图:

那么说明以j为中心的回文串一定在以po为中心的回文串的内部,且j和i关于位置po对称,由回文串的定义可知,一个回文串反过来还是一个回文串,所以以i为中心的回文串的长度至少和以j为中心的回文串一样,即Len[i] > = Len[j]。因为Len[j] < P-i,所以说i+Len[j]< P。由对称性可知Len[i]=Len[j]。
如果Len[j] >= P-i,由对称性,说明以i为中心的回文串可能会延伸到P之外,而大于P的部分我们还没有进行匹配,所以要从P+1位置开始一个一个进行匹配,直到发生失配,从而更新P和对应的po以及Len[i]
第二种情况: i>P
如果i比P还要大,说明对于中点为i的回文串还一点都没有匹配,这个时候,就只能老老实实地一个一个匹配了,匹配完成后要更新P的位置和对应的po以及Len[i]。

(3)时间复杂度分析

Manacher算法的时间复杂度分析和Z算法类似,因为算法只有遇到还没有匹配的位置时才进行匹配,已经匹配过的位置不再进行匹配,所以对于T字符串中的每一个位置,只进行一次匹配,所以Manacher算法的总体时间复杂度为O(n),其中n为T字符串的长度,由于T的长度事实上是S的两倍,所以时间复杂度依然是线性的。
下面是算法的实现,注意,为了避免更新P的时候导致越界,我们在字符串T的前增加一个特殊字符,比如说‘$’,所以算法中字符串是从1开始的。

伪代码如下:

Manacher(st[0...n], len){    mx <- 0;    ans <- 0;    po <- 0;    for i <- 1 to len do{    if mx >i then L[i] <- min(mx-i,L[2*po-i]);    else L[i] <- 1;    while st[i-L[i]] = st[i+L[i]] do L[i] <- L[i]+1;    if L[i] +i >mx then {        mx <- L[i]+i;        po<-i;}    ans <- max(ans, L[i]);        }      return ans-1;}

附上python实现代码:

class manacherAlogrithm(object):    def preDeal(self,str):        s=list(str)        for i in  range(0,len(s)*2+1,2):            s.insert(i,'#')        return s    def manacher(self,str):        mx =0        ans = 0        po = 0        s = self.preDeal(str)        L = [0]*len(s)        s.append('@')        for i in range(0,len(s)-2):            if mx > i:                L[i] = min(mx-i, L[2*po-i])            else :                L[i] = 1            while s[i-L[i]] == s[i+L[i]]:                L[i]= L[i]+1            if L[i]+i>mx:                mx = L[i]+i                po = i            if ans < L[i]:                sw = s[i-L[i]+1:i+L[i]-1]                ans = L[i]        sw = filter(lambda x:x!='#',sw)        sw = ''.join(sw)        return sw

送入Leetcode跑跑:
这里写图片描述

效果还行

附上LeetCode中最佳的代码,学习学习:

class Solution:        def longestPalindrome(self, s):        # Transform S into T.        # For example, S = "abba", T = "^#a#b#b#a#$".        # ^ and $ signs are sentinels appended to each end to avoid bounds checking        T = '#'.join('^{}$'.format(s))        n = len(T)        P = [0] * n        C = R = 0        for i in range (1, n-1):            P[i] = (R > i) and min(R - i, P[2*C - i])             # equals to i' = C - (i-C)            # Attempt to expand palindrome centered at i            while T[i + 1 + P[i]] == T[i - 1 - P[i]]:                P[i] += 1            # If palindrome centered at i expand past R,            # adjust center based on expanded palindrome.            if i + P[i] > R:                C, R = i, i + P[i]        # Find the maximum element in P.        maxLen, centerIndex = max((n, i) for i, n in enumerate(P))        return s[(centerIndex  - maxLen)//2: (centerIndex  + maxLen)//2]

跑了一下,也没提高多少跟帖子说的时间不一样(差距有些大),可能改了数据集吧,就不放结果了。

0 0