最长回文子串--力扣

吃可爱长大的小学妹 提交于 2020-01-10 04:10:33

5种解法:

1.最长公共子串

2.暴力法

3.动态规划

4.中心扩展法

5.Manacher法

以下记录大佬题解:

算法:
什么叫回文串?

如果一个字符串正着读和反着读是一样的,那它就是回文串。

中心扩展算法
我们观察到回文中心的两侧互为镜像。因此,回文可以从它的中心展开,并且只有 2n - 1 个这样的中心。

你可能会问,为什么会是 2n - 1 个,而不是 n 个中心?

因为回文的中心要区分单双。

假如回文的中心为 双数,例如 abba,那么可以划分为 ab bb ba,对于n长度的字符串,这样的划分有 n-1 种。

假为回文的中心为 单数,例如 abcd, 那么可以划分为 a b c d, 对于n长度的字符串,这样的划分有 n 种。

对于 n 长度的字符串,我们其实不知道它的回文串中心倒底是单数还是双数,所以我们要对这两种情况都做遍历,也就是 n+(n-1) = 2n - 1,所以时间复杂度为 O(n)。

当中心确定后,我们要围绕这个中心来扩展回文,那么最长的回文可能是整个字符串,所以时间复杂度为 O(n)。

所以总时间复杂度为 O(n^2)

代码如下:

    string longestPalindrome(string s) 
    {
        if (s.length() < 1)
        {
            return "";
        }
        int start = 0, end = 0;
        for (int i = 0; i < s.length(); i++)
        {
            int len1 = expandAroundCenter(s, i, i);//一个元素为中心
            int len2 = expandAroundCenter(s, i, i + 1);//两个元素为中心
            int len = max(len1, len2);
            if (len > end - start)
            {
                start = i - (len - 1) / 2;
                end = i + len / 2;
            }
        }
        return s.substr(start, end - start + 1);
    }

    int expandAroundCenter(string s, int left, int right)
    {
        int L = left, R = right;
        while (L >= 0 && R < s.length() && s[L] == s[R])
        {// 计算以left和right为中心的回文串长度
            L--;
            R++;
        }
        return R - L - 1;
    }
Manacher(马拉车) 算法
前面解法存在以下缺陷:

由于回文串长度的奇偶性造成了不同性质的对称轴位置,前面解法要对两种情况分别处理。
很多子串被重复多次访问,造成较差的时间效率,例如:
  字符:    a     b     a     b     a
  位置 :   0     1     2     3     4
当位置为 1 和 2 时,按中心扩展法,可以看出左边的 aba 分别被遍历了一次。

如果我们能改善重复遍历的不足,就很有希望能提高算法的效率。Manacher 正是针对这些问题改进算法。

解决单双两次遍历的问题
首先对字符串做一个预处理,在所有的空隙位置(包括首尾)插入同样的符号,要求这个符号是不会在原串中出现的。这样会使得所有的串都是奇数长度的,并且回文串的中心不会是双数,以插入#号为例:

aba  ———>  #a#b#a#
abba ———>  #a#b#b#a#
解决重复访问的问题
在前面的基础上,我们认为回文串的中心总是为 单数,我们把一个回文串中最左或最右位置的字符与其对称轴的距离称为回文半径,用 RL 表示。

用 RL[i] 表示以第 i 个字符为对称轴的回文串的回文半径。我们一般对字符串从左往右处理,因此这里定义 RL[i] 为第 i 个字符为对称轴的回文串的最右一个字符与字符 i 的距离,如 aba 的 RL[1]=2,即 ba。

对于上面插入分隔符之后的两个串,可以得到RL数组:

字符:    #     a     #     b     #     a     #
RL :     1    2     1     4     1     2     1
RL-1:    0    1     0     3     0     1     0
位置:     0    1     2     3     4     5     6

字符:    #     a     #     b     #     b     #     a     #
RL :     1     2     1     2     5     2     1     2     1
RL-1:    0     1     0     1     4     1     0     1     0
位置:     0     1     2     3     4     5     6     7     8
RL[i] 的大小总是定义为回文串最右的字符位置-回文串的对称轴字符位置+1,参看上图。

上面我们还求了一下 RL[i]-1。通过观察可以发现,RL[i]-1 的值,正是在原本那个没有插入过分隔符的串中,以位置 i 为对称轴的最长回文串的长度(注意,这里是全串的总长度,不要和 RL 半径混在一起了)。

于是问题变成了,怎样 高效地求的RL数组。基本思路是利用 回文串的对称性,扩展回文串。

我们再引入一个辅助变量 MaxRight,表示当前访问到的所有回文子串,所能触及的最右一个字符的位置。另外还要记录下 MaxRight 对应的回文串的对称轴所在的位置,记为 pos,它们的位置关系如下。

我们从左往右地访问字符串来求RL,假设当前访问到的位置为i,即要求RL[i],在对应上图,因为我们是从左到右遍历i, 而pos是遍历到的所有回文子串中某个对称轴位置(MaxRight最大时),所以必然有pos<=i,所以我们更关注的是,i是在MaxRight的左边还是右边。我们分情况来讨论。

1)当i在MaxRight的左边

可以用下图来刻画:

我们知道,图中两个红色块之间(包括红色块)的串是回文。

并且以i为对称轴的回文串,是与红色块间的回文串有所重叠的。

我们找到i关于pos的对称位置j,这个j对应的RL[j]我们是已经算过的。

根据回文串的对称性,以i为对称轴的回文串和以j为对称轴的回文串,有一部分是相同的。这里又有两种细分的情况。

1.1)以j为对称轴的回文串比较短,短到像下图这样

这时我们知道RL[i]至少不会小于RL[j],并且已经知道了部分的以i为中心的回文串,于是可以令RL[i]=RL[j] 为起始半径。

又因为(j + i) / 2 = pos ==> j = 2*pos - i 得到 RL[i]=RL2*pos - i]。

因此我们以RL[i]=RL2*pos - i]为起始半径`,继续往左右两边扩展,直到左右两边字符不同,或者到达边界。

1.2)以j为对称轴的回文串很长,超过了MaxRight在左侧的对称点

这时,我们只能确定,MaxRight - i 的部分是以i为对称轴的回文半径。

因此我们以RL[i] = MaxRight - i为起始半径,继续往左右两边扩展,直到左右两边字符不同,或者到达边界。

综上1.1 1.2分析,可以得出:在后面的代码中有体现

if (i < MaxRight)
{//  当i在MaxRight的左边
    RL[i] = min(RL[2 * pos - i], MaxRight - i);
}
2)当i在MaxRight的右边

遇到这种情况,说明以i为对称轴的回文串还没有任何一个部分被访问过,于是只能从i的左右两边开始尝试扩展了,也就是RL[i]=1。

当左右两边字符不同,或者到达字符串边界时停止。然后更新MaxRight和pos。

1)2) 分析结合的代码为:

if (i < MaxRight)
{// 1) 当i在MaxRight的左边
    RL[i] = min(RL[2 * pos - i], MaxRight - i);
}
else
{// 2) 当i在MaxRight的右边
    RL[i] = 1;
}


// 尝试扩展RL[i],注意处理边界
while (i - RL[i] >= 0  // 可以把RL[i]理解为左半径,即回文串的起始位不能小于0
    && i + RL[i] < len // 同上,即回文串的结束位不能大于总长
    && s1[i - RL[i]] == s1[i + RL[i]]// 回文串特性,左右扩展,判断字符串是否相同
    )
{
    RL[i] += 1;
}
为了得到字符串,我们还需要一个MaxRL来记录最大回文串的回文半径。MaxPos 来记录MaxRL对应的回文串的对称轴所在的位置。

由前面的分析可以知道, MaxRL- 1即为原始最大回文串的长度(注意,这里是全串的总长度,不要和RL半径混在一起了)。

原始最大回文串的起始位为(MaxPos - MaxRL + 1) / 2

代码如下:

string longestPalindrome(string s)
{
    int len = s.length();
    if (len < 1)
    {
        return "";
    }

    // 预处理
    string s1;
    for (int i = 0; i < len; i++)
    {
        s1 += "#";
        s1 += s[i];
    }
    s1 += "#";

    len = s1.length();
    int MaxRight = 0;                // 当前访问到的所有回文子串,所能触及的最右一个字符的位置
    int pos = 0;                    // MaxRight对应的回文串的对称轴所在的位置
    int MaxRL = 0;                    // 最大回文串的回文半径
    int MaxPos = 0;                    // MaxRL对应的回文串的对称轴所在的位置
    int* RL = new int[len];            // RL[i]表示以第i个字符为对称轴的回文串的回文半径
    memset(RL, 0, len * sizeof(int));
    for (int i = 0; i < len; i++)
    {
        if (i < MaxRight)
        {// 1) 当i在MaxRight的左边
            RL[i] = min(RL[2 * pos - i], MaxRight - i);
        }

作者:bian-bian-xiong
链接:https://leetcode-cn.com/problems/longest-palindromic-substring/solution/5-zui-chang-hui-wen-zi-chuan-cc-by-bian-bian-xiong/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!