leetcode5. Longest Palindromic Substring(dp,manacher算法)

题目链接

https://leetcode.com/problems/longest-palindromic-substring

解题思路:

求解最长公共子串问题

  • 暴力求解,时间复杂度 o ( n 3 ) o(n^3) o(n3)
  • 动态规划,时间复杂度 o ( n 2 ) o(n^2) o(n2)
  • 二分+字符串hash算法,时间复杂度 o ( n l o g ( n ) ) o(nlog(n)) o(nlog(n))
  • manacher算法,时间复杂度 o ( n ) o(n) o(n)
    本文着重介绍动态规划及manacher解法,二分+字符串hash解法思路见链接
动态规划求解:

d p [ i ] [ j ] = 1 dp[i][j]=1 dp[i][j]=1表示从 i i i j j j的字符串是一段回文字符串,反之若 d p [ i ] [ j ] = 0 dp[i][j]=0 dp[i][j]=0表示从 i i i j j j的字符串非回文字符串,那么有:
d p [ i ] [ j ] = { 1   d p [ i + 1 ] [ j − 1 ] = = 1   a n d   s [ i ] = = s [ j ] 0   e l s e dp[i][j]=\left\{ \begin{aligned} 1 &\ dp[i+1][j-1]==1 \ and \ s[i]==s[j] \\ 0 & \ else \\ \end{aligned} \right. dp[i][j]={10 dp[i+1][j1]==1 and s[i]==s[j] else
由于求解区间 [ i , j ] [i,j] [i,j]需要用到内部区间 [ i + 1 , j − 1 ] [i+1,j-1] [i+1,j1]的信息,故直接从0到s.size()顺序迭代更新 i i i j j j不可取,所以我们需要改变更新方式。
这里采用的是子串的长度和区间的左端点相结合更新的方式,先预处理子串长度为1和2的情况,然后按照上述公式更新即可。时间复杂度 o ( n 2 ) o(n^2) o(n2)

class Solution {
public:
    int dp[1010][1010];
    string longestPalindrome(string s) {
        memset(dp,0,sizeof(dp));
        for(int i=0;i<s.size();i++){
            dp[i][i]=1;
            if(i+1<s.size()&&s[i]==s[i+1]) dp[i][i+1]=1;
        }
        for(int L=3;L<=s.size();L++){
            for(int i=0;i+L-1<s.size();i++){
                int j=i+L-1;
                if(s[i]==s[j]&&dp[i+1][j-1]==1)
                    dp[i][j]=1;
            }
        }
        for(int L=s.size();L>=1;L--){
            for(int i=0;i+L-1<s.size();i++){
            int j=i+L-1;
            if(dp[i][j]){
                return s.substr(i,L);
                }
            }
        }
        
        return s;
    }
};
manacher算法求解

首先预处理,在字符串的首末和中间都加上分隔符号#,保证输入的子串长度是奇数。
具体原因见https://segmentfault.com/a/1190000003914228
利用 m x mx mx表示已经访问的所有回文子串中最右边的位置,
利用 p o s pos pos表示这个子串的回文中心,即有 m x = p o s + p [ p o s ] mx=pos+p[pos] mx=pos+p[pos] p [ p o s ] p[pos] p[pos]表示以 p o s pos pos为回文中心的字符串对应的回文半径,

其次求解字符串中第 i i i个位置对应的回文半径 p [ i ] p[i] p[i]
在求解和更新 p [ i ] p[i] p[i]时,需要注意:

  • 利用 i i i关于回文中心点 p o s pos pos的对称点的信息来简化计算的复杂度,不如设该点为 j j j,那么有:当 i i i p o s pos pos m x mx mx中间时:
    i − p o s = p o s − j i-pos=pos-j ipos=posj
    j = 2 ∗ p o s − i j=2*pos-i j=2posi
    此时 p [ i ] p[i] p[i]的信息可以更新为 p [ i ] = m i n ( p [ j ] , m x − i ) p[i]=min(p[j],mx-i) p[i]=min(p[j],mxi)

  • i i i m x mx mx右边,即i超出了 p [ p o s ] p[pos] p[pos]对应的回文半径,这时候以i为中心的回文字符串就是其本身,所以 p [ i ] = 1 p[i]=1 p[i]=1

  • 由于 i i i j j j关于 p o s pos pos对称,而 p [ i ] p[i] p[i]表示以 i i i为中心的子串的回文半径,显然仅靠 p [ j ] p[j] p[j]的信息是不够的,因为如果 i + p [ i ] > m x i+p[i]>mx i+p[i]>mx后,很可能出现 s [ i + p [ i ] ] = = s [ i − p [ i ] ] s[i+p[i]]==s[i-p[i]] s[i+p[i]]==s[ip[i]],所以需要进一步更新 p [ i ] p[i] p[i]

回文半径 p [ i ] − 1 p[i]-1 p[i]1中最大的值对应整个字符串最大的回文子串长度。
总体时间复杂度 o ( n ) o(n) o(n)

class Solution {
public:
    string longestPalindrome(string s) {
        if(s.size()<=1) return s;
        string t="",q="";
        for(int i=0;i<s.size();i++){
            t+="#";
            t+=s[i];
        }
        t+="#";
        
        vector<int> ans=solve(t);
        
        int maxv=-1,ind=-1;
        for(int i=0;i<ans.size();i++){
            if(ans[i]>maxv){
                maxv=ans[i];
                ind=i;
            }
        } 
        
        for(int i=ind-ans[ind]+1;i<=ind+ans[ind]-1;i++){
            if(t[i]!='#') q+=t[i];
        }
        return q;
    }
    
    vector<int> solve(string t){
        int pos=0,mx=0;
        int len=t.size();
        vector<int> p(len);
        for(int i=0;i<len;i++){
            if(i<mx) p[i]=min(p[2*pos-i],mx-i);//j=2*pos-i
            else p[i]=1;
            
            while(i-p[i]>=0&& i+p[i]<len && t[i+p[i]]==t[i-p[i]])//更新p[i]
                p[i]++;
            
            if(mx<i+p[i]-1){//更新最右边回文串对应的回文中心pos和右边界mx
                pos=i;
                mx=i+p[i]-1;
            }
        }
        return p;
    }
};
posted @ 2019-12-26 21:14  xzhws  阅读(39)  评论(0编辑  收藏  举报