Excaliburer`s Zone

It was challenging, but not risky.

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

一.题目链接:https://leetcode.com/problems/longest-palindromic-substring/

二.题目大意:

  给定一个字符串,找出它最长的回文子串。例如,字符串“caabb”,它的最长回文子串为“aabb”。

三.题解:

  找最长回文子串应该说是比较经典的题目,这个题目我目前有三种思路:

方法1:暴力解决,找出所有的子串,并判断子串是不是回文,然后记录最长的回文子串。代码如下:

class Solution {
public:
    string longestPalindrome(string s) {
        int len = s.size();
        string rs;//用于保存最长的回文子串
        int max_len = 0;
        for(int i = 0;i < len;i++)
            for(int j = i + 1;j <= len;j++)
        {
          string temp = s.substr(i,j - i);

            if(isPalin(temp) == true)
            {
                if(max_len < temp.size())
                {
                    max_len = temp.size();
                    rs = temp;
                }
            }
        }
    return rs;
    }
    bool isPalin(string s)
    {
        int len = s.length();
        int flag = 1;
        for(int i = 0;i < len;i++)
        {
            if(s[i] != s[len - 1 - i])
                flag = 0;
        }
        if(flag)
            return true;
        return false;
    }

};

 其中遍历所有的子串需要的时间复杂度为O(n2),判断子串是不是回文串的时间复杂度为O(n),所以总的时间复杂度为O(n3)。

提交结果:Time Limit Exceeded(超时).

方法2:

  以字符串中的每个字符为中心向两边扩展,从而找到最长的回文子串。其中回文子串存在两种情况:(1)形如"aabaa"这种中间只有一个字符的回文子串。(2)形如"aabbaa"这种中间有两个或多个字符的回文子串。所以在处理的时候,先优考虑第二种情况,可以吧第二中情况中的中间重复字符看成一个字符,然后剩下的部分同第一种情况就可以进行相同的处理了。代码如下:

class Solution {
public:
    string longestPalindrome(string s) {
        int len = s.size();
        int max_len = 0;
        string rs = "";
        for(int i = 0;i < len;i++)
        {
            int f = i,b = i;
            int df = 0;
            int oddf = 0;
            while(s[b] == s[i])//中间字符存在重复的情况
            {
                b++;
                oddf = 1;
            }
            if(oddf == 1)
                b--;
            while(f >= 0 && b < len && s[f] == s[b])//中间字符的左右两边字符相等,两边都增长
            {
                f--;
                b++;
                df = 1;
            }
            if(df == 1)
            {
                f++;
                b--;
            }
            string temp = s.substr(f,b-f+1);
            if(max_len < temp.size())
            {
                max_len = temp.size();
                rs = temp;
            }

        }
        return rs;
}
};

  

这个程序中的我设置了两个哨兵,用来判断是否发生了以下情况:

(1)中间字符连续几个都相同或重复,此时下标b增加。

(2)中间字符的左右两边字符相等,所以两边都增长,此时b增加,f减小。

由于每次增长后,下标f(或b)都会再次-1(+1),相当于多减少(增加)了一次,所以需要增加(减少)一次,来恢复成为正常的下标。 

这种方法的时间复杂度为O(n2),空间复杂度为O(n)。

提交结果:Accepted(16ms).

方法3:

  利用动态规划的思想,将父问题拆分为若干个子问题,用dp[i][j]来表示字符串下标为[i,j]的子串是否为回文串,那么有以下的分析:

(1)如果i == j表示子串是一个字符,那么此时必然是一个回文串。

(2)如果相邻的字符相等(这种情况实质就是方法2中的中间字符重复的情况),j == i+1,此时要判断s[i]与s[j]是否相等,如果相等,那么该子串也是回文串,

(3)判断剩下的情况,如果(s[i],s[i+1],.....,s[j])为回文串的话,那么(s[i+1],s[i+2],.....s[j-1])必然也是一个回文串,且s[i] == s[j]。

所以,初始状态为:

dp[i][i] =1。

整个状态方程为:

代码如下:

class Solution
{
public:
    string longestPalindrome(string s)
    {
        int len = s.size();
        string rs = "1";
        int start = 0;
        int max_len = 1;//此处的初始值为1,默认为dp[i][i]
        int dp[1000][1000]= {0};
        for(int i = 0; i < len; i++)
        {
            dp[i][i] = 1;//初始状态,在求解前必须初始化
            for(int j = 0; j < i; j++)
            {

                 if(i == j+1)
                {
                    if(s[i] == s[j])
                        dp[j][i] = 1;
                }
                else
                {
                    if(s[i] == s[j])
                    {
                        dp[j][i] = dp[j+1][i -1];
                    }
                }
                if(dp[j][i] && max_len < (i - j +1))
                {
                    max_len = (i - j +1);
                    start = j;
                }
            }
        }
        rs = s.substr(start,max_len);
        return rs;
    }

};

这种方法有需要注意的几点:

(1)最大长度的初始值为1,而不是0,默认为dp[i][i]这种形式。

(2)由于求解父问题实质,是将父问题分解成为若干个子问题,所以子问题必须被解决,才能求解父问题。所以,一定要利用初始状态对dp进行初始化,即dp[i][i]=1。(动态规划类问题都要初始化初始状态)

(3)在这个程序中,利用dp[j][i]来代替理论上的dp[i][j],这是因为第一层for循环还没取到所有值的时候,第二层for循环已经取了所有可能的值了,这就可能导致子问题还没解决,就去求解父问题了。例如:

求dp[0][4]时,需用到dp[1][3],如果用常规的for循环的话,i=1肯定比i=0出现的晚,导致父问题求解出错。(这可以看成动态规划类问题的常用的技巧)

提交结果:Accepted( 212 ms). 比方法2慢好多...

========================================经查知,还有一种更优的算法============================================================================================================================

方法4:

就是有名的Manacher算法了,感觉这个算法真的不容易想到,这个算法的时间复杂度直接达到了常数级别,即O(n)。理解一下这个算法,对思维的锻炼还是挺不错的。Manacher算法的大致思路如下:

1.预处理:

  (1)对于一个输入的字符串s,把字符串中的任意两个字符之间插入一个"#"(也可以是其他字符),第一个字符之前和最后一个字符之后也要插入。这样就生成了一个新字符串,例如:对于字符串aabbcc,经过处理后,就变成了#a#a#b#b#c#c#。这么做的好处就是不用考虑字符串的奇偶性了,所有的字符串的长度都变成了奇数。

  (2)对于经过第一步处理后的字符串,在该字符串的首位各添加一个字符,但首尾字符一定不能是相同的。(如果是相同的话,在判断最后一个#时,会把这两个字符也考虑进去)。例如:对于#a#a#b#b#c#c#,首位字符分别添加为"%"和"$",则最终经过预处理后的字符串就变成了%#a#a#b#b#c#c#$。这么做的好处是为了防止判断字符串时发生越界。

2.构造一个数组P,其中P[i]表示的是以字符s1[i]为中心最长的回文串向左/向右扩展的长度(包括s[i],可以理解成其"半径"),P数组有一个性质:P[i] - 1是是该回文子串在原字符串中的长度(即在s中的长度)。为什么呢?以下是证明:

原字符串是s经过预处理后,变成了长度为奇数的字符串s1,已知P[i],则以s1[i]为中心的最长回文串的长度为2*p[i] -1(回文子串的长度必然为奇数),其中一定有p[i]个分隔符"#"(观察可知),所以该回文子串在原字符串s中的长度为p[i] - 1。所以求出数组P这个问题基本就解决了。

3.如何求解数组P呢?这里需要引入两个变量,id和mx,其中id表示某个回文串的中心,并且该回文串满足这样一个性质:它右边界是目前所有回文串中最大的。而mx表示的就是这个回文串的右边界的下标。对于数组p,我们可以通过mx和id以及之前已经求出的p[i]的值来求解,具体如下:

先从左至右依次计算P[i],但计算P[i]时,P[j](j<i)已经计算完毕。此时分为两种情况:

(1)i < mx.

1、 当i < mx 时,如下图。此时可以得出一个非常神奇的结论p[i] >= min(p[2*id - i], mx - i),下面我们来解释这个结论

 

image

如何根据p[j]来求p[i]呢,又要分成两种情况

 

(1.1)当mx – i > p[j], 这时候以S[j]为中心的回文子串包含在以S[id]为中心的回文子串中,由于 i 和 j 对称,以S[i]为中心的回文子串必然包含在以S[id]为中心的回文子串中,所以 此时P[i]一定是等于p[j]的。如下图

注:这里p[i]一定等于p[j],后面不用再匹配了。因为如果p[i]后面还可以继续匹配,根据对称性,p[j]也可以继续扩展了。

 

image

 

(1.2)当mx – i <= p[j], 以S[j]为中心的回文子串不完全包含于以S[id]为中心的回文子串中,但是基于对称性可知,下图中两个绿框所包围的部分是相同的,也就是说以S[i]为中心的回文子串,其向右至少会扩张到mx的位置,也就是说 P[i] 至少等于 mx - i,至于mx之后的部分是否对称,就只能老老实实去匹配了。

注:如果mx – i < p[j] ,这时p[i]一定等于mx - i, 因为如果p[i]在mx之后还可以继续匹配,根据对称性,mx之后匹配的点(包括mx)一定会出现在my的前面,这说明p[id]也可以继续扩展了

 

image

 

所以,最后p[i]去最小值(之后的值要去匹配),即p[i] = min(p[2*id - i], mx - i).

(2)i >= mx.

此时并不能利用已知的信息来求解P[i],此时默认P[i]=1,剩下的部分通过匹配来求解P[i]。

该方法的代码如下:

class Solution {
public:
    string longestPalindrome(string s) {
        int len = s.size();
        if(len <= 1)return s;  
        //对s进行预处理
        string temp_str = "#";
        for(int i = 0; i < len; i++)
        {
            temp_str += s[i];
            temp_str += "#";
        }
        temp_str += "^";
        string st_str = "";
        st_str += "%";
        st_str += temp_str;
        return Manacher(st_str,s);
    }
    //马拉车算法,时间复杂度为O(n)
    string Manacher(string s1,string s2)
    {
        int p[3000] = {0};
        int id = 0, mx = 0;
        int len = s1.size();
        int max_len = 0;
        int flag = 0;
        for(int i = 1; i < len - 1; i++)
        {

            if(i < mx)
            {
                if(p[2 * id - i] < (mx - i))
                    p[i] = p[2 * id - i];
                else//即p[2*id-i] >= (mx -i)
                    p[i] = mx - i;
            }
            else
            {
                    p[i] = 1;
            }
           //通过匹配计算p[i]
            while(s1[i + p[i]] == s1[i - p[i]])
                p[i]++;
           //更新mx和id的值
            if(p[i] + i > mx)
            {
                mx = p[i] + i - 1;
                id = i;
            }
            if(p[i] > max_len)
            {
                max_len = p[i];
                flag = i - p[i];
            }

        }
         return s2.substr(flag/2, max_len - 1);

    }
};            

这段代码中有几个需要注意的地方:

1.P[i]的计算过程可以不通过分情况考虑,直接用P[i] = min(p[2*id-i],mx-i)来求解,我写成这样是为了更直观的去理解。

2.实际上通过匹配计算P[i],都是mx - i<= p[2*id-i]的情况,只有超过mx的地方才通过匹配计算p[i],所以它的时间是严格的线性的。

3.mx = p[i] +i -1,因为p[i]实际上也把s[i]也考虑进去了,所以此处还要减去1。

4.在最后计算最长回文子串在原字符串中的部分时,起始点为i-p[i]/2,(这也是通过观察的得知的),这一点需要注意。

提交结果:Accepted( 6ms).确实很快...

参考:

http://blog.csdn.net/suool/article/details/38383045

posted on 2017-12-04 16:02  Excaliburer  阅读(227)  评论(0编辑  收藏  举报