5.最长回文子串

5. 最长回文子串

题目描述

给你一个字符串 s,找到 s 中最长的回文子串。

示例1
输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。
示例2
输入:s = "cbbd"
输出:"bb"
示例3
输入:s = "a"
输出:"a"
示例4
输入:s = "ac"
输出:"a"
提示:

提示:

  • 1 ≤ s . l e n g t h ≤ 1000 1 \le s.length \le 1000 1s.length1000
  • s 仅由数字和英文字母(大写和/或小写)组成

题解:

Manacher算法,本质仍是暴力匹配。参考自: Manacher算法详解

应对偶数回文串方法:预处理,将原字符串的首部和尾部以及每两个字符之间插入一个特殊字符。

这一步预处理操作后的效果就是原字符串的长度从 n 改变成了 2*n+1,也就得到了我们需要的可以去做暴力扩展的字符串,并且从 预处理后的字符串得到的最长回文字符串的长度除以2 就是 原字符串的最长回文子串长度,也就是我们想要得到的结果。

概念:

回文半径和回文直径:因为处理后回文字符串的长度一定是奇数,所以回文半径是包括回文中心在内的回文子串的一半的长度,回文直径则是回文半径的2倍减1。比如对于字符串 “aba”,在字符 ‘b’ 处的回文半径就是2,回文直径就是3。

最右回文边界R:在遍历字符串时,每个字符遍历出的最长回文子串都会有个右边界,而R则是所有已知右边界中最靠右的位置,也就是说R的值是只增不减的。

回文中心C:取得当前R的第一次更新时的回文中心。由此可见R和C时伴生的。

半径数组:这个数组记录了原字符串中每一个字符对应的最长回文半径。

流程:

  • 预处理原字符串

    先对原字符串进行预处理,预处理后得到一个新的字符串,这里我们称为S,为了更直观明了的让大家理解Manacher的流程操作,我们在下文的S中不显示特殊字符(这样并不影响结果)。

  • R 和 C 的初始值为 -1 ,创建半径数组 pArr

    这里有点与概念相差的小偏差,就是R实际是最右边界位置的右一位。

  • 开始从下标 i = 0 去遍历字符串 S

    1. i > R,也就是 i 在 R 外,此时没有什么花里胡哨的方法,直接暴力匹配,记得看看 C 和 R 是否需要更新

    在这里插入图片描述

    1. i <= R,也就是 i 在 R 内,此时分三种情况,在讨论这三种情况前,我们先构建一个模型

      在这里插入图片描述

      L 是当前 R 关于 C 的对称点,i’ 是 i 关于 C 的对称点,可知 i’=2*C-i,并且我们发现,i’ 的回味区域是我们已经求过的,从这里我们就可以开始判断是不是可以进行加速处理了

      2.1 i’ 的回文区域在 L-R 的内部,此时 i 的回文直径与 i’ 相同,我们可以直接得到 i 的回文半径,下面给出证明:

      在这里插入图片描述
      红线部分是 i’ 的回文区域,因为整个 L-R 就是一个回文串,回文中心是 C ,所以 i 形成的回文区域和 i’ 形成的回文区域是关于 C 对称的

      2.2 i’ 的回文区域左边界超过了 L ,此时 i 的回文半径则是 i 到 R ,下面给出证明:

      在这里插入图片描述

      首先我们设 L 点关于 i’ 的对称点为 L’,R点关于 i 点对称的点为 R’,L 的前一个字符为 x ,L’ 的后一个字符为 y ,k 和 z 同理。此时我们知道 L-L’ 是 i’ 回文区域内的一段回文串,故 R’-R 也是回文串,因为 L-R 是一个回文串,而它们都是对称关系。所以我们得到一系列关系:x=y,y=k,x!=z ,所以 k!=z 。这样可以验证 i 点的回文半径是 R-i

      2.3 i’ 的回文区域左边界恰好和 L 重合,此时 i 的回文半径最少是 i 到 R,回文区域从 R 继续向外部匹配,下面给出证明:

      在这里插入图片描述
      因为 i’ 的回文左边界和 L 重合,所以已知的 i 的回文半径就和 i’ 的一样了,我们设 i 的回文区域右边界下一个字符是 y ,i 的回文区域左边界的上一个字符是 x ,现在我们只需要从 x 和 y 的位置开始暴力匹配,看是否能够把 i 的回文区域扩大即可。

Manacher算法代码模板:

#include <cstdio>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 500001;

char s1[N];
char s2[N << 1];
int p[N << 1];

int main(void) {
    scanf("%s", s1);
    int len = strlen( s1 ) * 2 + 1;
    
    int k = 0;
    for ( int i = 0; i < len; ++i ) {
        s2[i] = ( i & 1 ) == 0 ? '#' : s1[k++];
    }
    int r = -1, c = -1;
    int ret = 0;
    for ( int i = 0; i < len; ++i ) {
        p[i] = r > i ? min( r - i, p[2 * c - i]) : 1;
        while ( i + p[i] < len && i - p[i] > -1 ) {
            if ( s2[i + p[i]] == s2[i - p[i]] ) ++p[i];
            else break;
        }
        if ( i + p[i] > r ) {
            r = i + p[i];
            c = i;
        }
        ret = max( ret, p[i] - 1 );
    }
    printf("%d\n", ret);
    return 0;
}

回到这题上来,我们首先需要求出半径数组,找到最大的值 ret(减1表示实际的回文子串长度) 和下标 idx ,最长回文子串是:s[(idx - ret + 1) / 2, ret] 。

代码:

class Solution {
public:
    void manacherString( vector<char>& t, string& s ) {
        int n = s.length() * 2 + 1;
        t.resize(n);
        int k = 0;
        for ( int i = 0; i < n; ++i )
            t[i] = (i & 1) ? s[k++] : '#';
    }
    string manacher( const vector<char>& t, const string& s ) {
        int r = -1, c = -1;
        int ret = 0, len = t.size(), idx = -1;
        vector<int> p( t.size() );
        for ( int i = 0; i < t.size(); ++i ) {
            p[i] = r > i ? min( r - i, p[2 * c - i] ) : 1;
            while ( i + p[i] < len && i - p[i] >= 0 ) {
                if ( t[i + p[i]] == t[i - p[i]] ) ++p[i];
                else break;
            }
            if ( i + p[i] > r ) {
                r = i + p[i];
                c = i;
            }
            if ( p[i] > ret ) {
                ret = p[i];
                idx = i;
            }
        }
        ret -= 1;
        return s.substr( (idx - ret + 1) >> 1, ret );
    }
    string longestPalindrome(string s) {
        if ( !s.size() ) return "";
        vector<char> t;
        manacherString( t, s );
        return manacher( t, s );
    }
};
/*
时间:16ms,击败:94.99%
内存:8.7MB,击败:73.13%
*/
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值