[LeetCode] 2 Longest Palindromic Substring 最长回文子串

[LeetCode]2 Longest Palindromic Substring 最长回文子串

Description

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.

Example

Example 1:
Input: "babad"
Output: "bab"
Note: "aba" is also a valid answer.
Example 2:
Input: "cbbd"
Output: "bb"

题意

给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。存在唯一的最长回文子串。

题解

这道题目是要求输出最长回文子串,利用Manacher's Algorithm可以将时间复杂度提升到O(n)的惊人水平。下面首先介绍下该算法:

第一步,先对字符串进行一步预处理,做法是在每个字符的两侧添加数据中不会出现的符号(例如:#),"bob" --> "#b#o#b#" ,"noon" --> "#n#o#o#n#"。
这样做的目的是无论给出的字符串的长度的奇偶性,处理后得到的字符串都是奇数个,这样就可以不用讨论奇偶性的问题了。

第二步,我们需要处理和新字符串s等长的数组p,其中p[i]表示以s[i]字符为中心的回文子串的半径,若p[i] = 1,则该回文子串就是s[i]本身,一个简单的例子:

s[i]: # 1 # 2 # 2 # 1 # 2 # 2 #
p[i]: 1 2 1 2 5 2 1 6 1 2 3 2 1

为什么需要关注回文子串的半径呢?以上面的例子中中间的1为例,其回文半径为6,而未添加#号的回文子串为 "22122",长度是5,为回文半径减1。原因很简单,可以分两种情况考虑,如果回文串长度为奇数,那么单侧的半径是(长度+1)/2,又因为单侧填充字符,所以填充字符后的回文半径为((长度+1)/2)x2=长度+1,如果长度为偶数,那么单侧的半径是长度/2,又因为两侧填充字符,所以填充字符后的回文半径为(长度/2)x2+1=长度+1

然后看中间的 '1' 在字符串 "#1#2#2#1#2#2#" 中的位置是7,而半径是6,7-6=1,刚好就是回文子串 "22122" 在原串 "122122" 中的起始位置1。那么我们再来验证下 "bob","o" 在 "#b#o#b#" 中的位置是3,但是半径是4,3-4=-1,出现了错误。所以我们应该至少把中心位置向后移动一位,才能得到正确的解,我们需要在前面增加一个字符,这个字符不能是#号,也不能是s中可能出现的字符,所以一般会用$添加到开头。这样的话,只要拥有p数组,我们就可以得到最长回文子串的长度和起始位置。

第三步,我们要介绍如何求取数组p。首先我们需要增加两个辅助变量,id和mx。其中id是指能延申到最右端位置的那个回文串的中心位置,mx是回文串能够延申到的最右端位置。
然后算法的核心是:
如果 mx > i, 则 p[i] = min( p[2 * id - i] , mx - i )
否则,p[i] = 1
j=2*id-i,表示i以id对称的点 ,因为 j 到 id 之间到距离等于 id 到 i 之间到距离,为 i - id,所以 j = id - (i - id) = 2*id - i。

if(mx>i):

当 mx - i > P[j] 的时候,以S[j]为中心的回文子串包含在以S[id]为中心的回文子串中,由于 i 和 j 对称,以S[i]为中心的回文子串必然包含在以S[id]为中心的回文子串中,所以必有 P[i] = P[j],其中 j = 2*id - 1。


当 mx - i<=P[j] 的时候,以S[j]为中心的回文子串不一定完全包含于以S[id]为中心的回文子串中,但是基于对称性可知,上图中两个绿框所包围的部分是相同的,也就是说以S[i]为中心的回文子串,其向右至少会扩张到mx的位置,也就是说 P[i] = mx - i。至于mx之后的部分是否对称,就只能去匹配了,这就是后面紧跟到while循环的作用。

else:
对于 mx <= i 的情况,此时镜像对预判位置起不到作用,只能从长度为1开始对比,所以p[i]=1

以上就是Manacher's Algorithm的全部内容。

对于本题而言要求输出最长回文子串,利用Manacher's Algorithm处理字符串,中途保留记录最大的p[i]值已经对应的索引位置i。将字符串拆分即可。

代码

class Solution {
public:
    string longestPalindrome(string s) {
        string str = "$#";
        int le = s.size();
        for(int i=0;i<le;i++){
            str+=s[i];
            str+='#';
        }
        int len = str.size();
        int P[len]={0},id=0,mx=0,resMax=0,resId=0;
        for(int i=1;i<len;i++){
            if(mx>i){
                P[i]=min(P[2*id-i],mx-i);
            }else{
                P[i]=1;
            }
            while(str[i+P[i]]==str[i-P[i]]) ++P[i];
            if(mx<i+P[i]){
                mx = i + P[i];
                id = i;
            }
            if (resMax< P[i]) {
                resMax = P[i];
                resId = i;
            }
        }
        return s.substr((resId - resMax) / 2, resMax - 1);
    }
};

猜你喜欢

转载自www.cnblogs.com/caomingpei/p/10612847.html