5. 最长回文子串

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

示例 1:

输入: "babad"
输出: "bab"
注意: "aba" 也是一个有效答案。

示例 2:

输入: "cbbd"
输出: "bb"

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/longest-palindromic-substring
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

这个问题有几种解法,一种是性能最差的,暴力破解。就是把所有的回文子串一个个的比较获取出来,然后找出一个最大的。第二种是找最长公共子串,就是把字符串倒过来,然后找这两个字符串的公共子串,这种方法求得公共子串并不一定是回文,需要再判断,并且效率没有提升太多。第三种方法是动态规则,就是用一个二维数组保存每一个字符与另外一个是否相同,生成数组后,根据数组判断最长的字符。这三种感觉既占了空间效率也没有明显改善,所以就没实现。

我的解,中心扩散法,回文的特点是两边一样,所以可以根据这个判断,就是一个字符一个字符的遍历,然后判断当前字符与下一个和下下个字符是否相等,如果相等,就是回文字符,然后向两边延伸,直到不相等,与最大的判断,获取最长的位置。

class Solution {
public:
    string longestPalindrome(string s) {
        int sstart = 0;
        int send = 0;
        for(int i = 0; i < s.size(); i++)
        {
            int j = i + 1;
            if(j < s.size() && s[i] == s[j])
            {
                int ti = i;
                int tj = j;
                while(ti >= 0 && tj < s.size())
                {
                    if(s[ti] != s[tj])
                    {
                        break;
                    }
                    ti--;
                    tj++;
                }
                ti++;
                tj--;
                if(send - sstart < tj - ti)
                {
                    sstart = ti;
                    send = tj;
                }
            }
            j = j + 1;
            if(j < s.size() && s[i] == s[j])
            {
                int ti = i;
                int tj = j;
                while(ti >= 0 && tj < s.size())
                {
                    if(s[ti] != s[tj])
                    {
                        break;
                    }
                    ti--;
                    tj++;
                }
                ti++;
                tj--;
                if(send - sstart < tj - ti)
                {
                    sstart = ti;
                    send = tj;
                }
            }
        }
        return string(s, sstart, send - sstart + 1);
    }
};

这个方法是比较好理解的,并且空间复杂度只有o(1),时间复杂度虽然是o(n^2),但是子串判断的时候,一次跳两个位置,比上面的o(n^2)要少一些步骤。

除了这个方法之外,还有一个求解最长子串的公认算法,就是Manacher。这个算法看了好几个题解,查了很多,最后找到 https://leetcode-cn.com/problems/longest-palindromic-substring/solution/zhong-xin-kuo-san-dong-tai-gui-hua-by-liweiwei1419/ 这个题解讲的比较清楚。感觉作者说的挺对的,是对中心扩散法的修正,为什么呢?因为中心扩散法在判断的索引不断后移中,有很多一开始遍历过的结果又被重新查询了一边。

比如下面的字符串

当我们判断到e的时候,其实是知道当前的回文字符串是abcecba,如果我们判断e的下一个c的时候,我们又需要循环判断一下c作为回文字符串中心的情况,虽然这个例子中没有判断太多,但是实际上,因为是回文字符串,也就像镜子一样,并且c是包含在前面e为中心的回文字符串中,那么c的回文字符串的情况与e左边刚刚判断完的c的情况是一样的,至少有很大一部分是可以利用的。也就是e左边的c是不是回文字符串,字符串有多长,与e的右边的c是一样的,最起码在e的回文字符串里面是一样的,这样就可以利用并且较少判断。

为了更方便计算,Manacher算法会在每一个字符串左右增加一个分隔符,比如'#',然后求一个数组p,p就是当前索引字符串在的位置的回文半径。这样求完之后,直接遍历p找到最大的就可以了。求数组p的时候,就用到了上面的思路,如果某个索引在某一个回文字符串内,那么就可以通过镜像找到对应位置的数据,然后根据边界做一些处理就可以了。我一直在想,不增加分隔符是不是也可以,这样就减少了插入数据的时间和空间消耗,有待测试。

 

string addboundaries(string s)
{
    string tmp;
    for (auto& iter : s)
    {
        tmp = tmp + "#" + iter;
    }
    tmp = tmp + "#";
    return tmp;
}
string longestPalindrome(string s)
{
    string tmpstr = addboundaries(s);
    int slen = tmpstr.size();
    int *p = new int[slen];
    memset(p, 0, slen * sizeof(int));
    int maxright = 0;
    int center = 0;
    int maxlen = 1;
    int start = 0;
    for (int i = 0; i < slen; i++)
    {
        if (i < maxright)
        {
            int mirror = 2 * center - i;
            p[i] = min(maxright - i, p[mirror]);
        }
        int left = i - (1 + p[i]);
        int right = i + (1 + p[i]);
        while (left >= 0 && right < slen && tmpstr[left] == tmpstr[right])
        {
            p[i]++;
            left--;
            right++;
        }
        if (i + p[i] > maxright)
        {
            maxright = i + p[i];
            center = i;
        }
        if (p[i] > maxlen)
        {
            maxlen = p[i];
            start = (i - maxlen) / 2;
        }
    }
    return string(s, start, maxlen);
}

上面代码可以看出,

插入分隔符,方便计算

申请一个数组p,保存数据

保存计算到最有边界的位置索引(针对插入分隔符之后的数组)maxright

保存针对最有边界位置的回文字符串的中心位置center

原字符串中最长回文字符串的长度maxlen

原字符串中最长回文字符串的起始位置start

开始遍历,如果i比maxright小,说明i的一部分数据已经可以通过镜像得到,不需要计算了。只需通过中心法判断基于当前位置i超过maxright的字符串,因为这部分本来也没计算过,所以并没有重复计算,如果满足了回文字符串,就把p[i]实时更新,直到不满足。这时判断最右边界,更新最右边界,中心位置,最大字串长度和起始位置。

当时对于这个算法没仔细想过,一直担心会有逻辑错误,比如,

  • 在更新center右边的p数组时,会不会影响到左边的数组
  • 会不会有情况使得center左边的回文字符串的最右边界比maxright大
  • 如果在center右边的i位置,回文字符串最右边界大于maxright,那么从i到当前maxright之间的字符镜像的位置就会改变,会不会出现更新后镜像位置的数组长度不是最优的

第一条,右边数组修改,并不影响左边,因为从左边遍历过来,每一个位置已经通过中心法找到了最长的位置,所以不管怎么修改其他的数组,如果已经遍历过,肯定是最长的,最优解

第二条,还是上面的解释,因为每个遍历都是最优解,并且记录了遍历过的所有字符串最右边界,所以maxright是最右边的位置,不存在center左边的回文字符串长度超过maxright

第三条,做了好几个符合条件的字符串,发现边界都被处理了,并不会出现矛盾的情况,也就是按照这个逻辑是可以覆盖所有情况,并且是正确的

 

 比如这个字符串,如果当前center是x,maxright是e,就是第二行浅灰色的表示,如果当前走到了i位置,也就是标红的w,那么可以以判断i的最右边界超过了第二行的标记,更新后是第一行的标记,center到i的位置,maxright在原来的基础上增加了1,那么原来的i的下一个字符e(棕色),原来针对x的对应是最左边的e,长度是4,现在更新后变成了w左边的e,长度是0,这样发现确实是0,是正确的,为什么呢,因为在求x的时候到了e的位置就结束了,也就是e的右边与x对称轴e的左边并不一样,所以也不可能是4.

另外我也试着不增加分隔符重写这个编码,立马发现了分隔符的作用,统一计算,减少条件判断,分隔符就是为了把所有的回文字符串都改成奇数的,这样就可以确定center和maxright,如果没有分隔符,那么每一个子回文字符串都要额外判断偶数的情况,太复杂。

posted @ 2019-12-10 20:31  秋来叶黄  阅读(163)  评论(0编辑  收藏  举报