最长回文子串的五种求法(暴力、中点扩散、DP、hash+二分、Manacher)

暴力枚举法 O ( n 3 ) O(n^3) O(n3)

暴力大法好,枚举字符串的始位置和末位值,再判断始位置和末位值之间的字符串是否为回文串,这种做法的时间复杂度为O(n^3)。

中点扩散算法 O ( n 2 ) O(n^2) O(n2)

在这里插入图片描述

由于回文串是对称的,所以我们可以枚举所有的中点,再向两侧扩散,时间复杂度为O(n^2)。

例题链接
在这里插入图片描述
代码如下:

//中点扩散算法
#include<bits/stdc++.h>
using namespace std;
const int N=1e3+10;
int dp[N][N];
int main()
{
    string str;
    getline(cin,str);
    int res=0;
    for(int i=0;i<str.length();i++)
    {
        int l=i-1,r=i+1;//判断奇数长度回文串
        while(l>=0&&r<str.length()&&str[l]==str[r]) l--,r++;
        res=max(res,r-l-1);
        l=i,r=i+1;//判断偶数长度回文串
        while(l>=0&&r<str.length()&&str[l]==str[r]) l--,r++;
        res=max(res,r-l-1);
    }
    cout<<res<<endl;
    return 0;
}

动态规划 O ( n 2 ) O(n^2) O(n2)

用动态规划来做上面的那个例题,dp[N][N]来存状态,dp[ i] [ j ]=1,表示字符串从i到j为回文串。 (1)当str[ i ]==str[ j ]时,dp[i+1][j-1]=1,则dp[ i ][ j ]=1 ( [i+1,j-1]为回文串,且str[ i ]==str[ j ]则[i,j]为回文串 ),否则dp[ i ][ j ]=0。

(2)当str[ i ]!=str[ j ]时,dp[ i ][ j ]=0。

可以得到状态转移方程

d p [ i ] [ j ] = { d p [ i + 1 ] [ j − 1 ] s t r [ i ] = = s t r [ j ] 0 s t r [ i ] ! = s t r [ j ] dp[i][j]=\begin{cases}dp[i+1][j-1]&str[i]==str[j]\\0&str[i]!=str[j]\end{cases} dp[i][j]={dp[i+1][j1]0str[i]==str[j]str[i]!=str[j]

在遍历方面也要注意,如果按照i和j从小到大的顺序来枚举子串的两个端点,然后更新dp[i]lj],会无法保证dp[i + 1][ - 1]已经被计算过,从而无法得到正确的dp[i][i]。我们可以遍历长度,长度从小到达依次增加,每次判断出左右端点即可。

代码如下:

//动态规划
#include<bits/stdc++.h>
using namespace std;
const int N=1e3+10;
bool dp[N][N];
int main()
{
    string str;
    getline(cin,str);//读入字符串
    int res=1;
    int n=str.length();
    for(int i=0;i<n;i++)
    {
        
        dp[i][i]=true;
        if(i<n-1&&str[i]==str[i+1])//判断边界条件
        {
            res=2;
            dp[i][i+1]=true;
        }
    }
    for(int l=3;l<=n;l++)//遍历字符串长度,最小从3开始(最小为1,加上左右两边)
    {
        for(int i=0;i+l-1<n;i++)//左边界
        {
            int j=i+l-1;//右边界
            if(str[i]==str[j]&&dp[i+1][j-1])
            {
                dp[i][j]=1;
                res=l;//更新时,一定l>=res
            }
                
        }
    }
    cout<<res<<endl;
    return 0;
}

hash+二分 O ( n l o g   n ) O(nlog\ n) O(nlog n)

题目链接
在这里插入图片描述

这道题的数据范围较大,不能采用前几种做法。可以用hash+二分来做,可以用二分来做的原因是,回文串存着一种单调性,通过hash可以方便的判断出两个字符串是否相等。遍历每个字符,然后二分其回文串能最长长度,hash判断其字符串是否相等。
在存hash值时,可以存一个正向的hash数组和一个逆向的hash数组。
为了方便代码书写,可以在字符串两两之间添加一个未出现的字符,这样就不用在写代码时分别讨论回文串长度奇偶的情况。
整体算法的时间复杂度为O(n log n)。

代码如下:

#include<bits/stdc++.h>
using namespace std;
typedef unsigned long long ull;
const int N=1e6+10;
const int base=131;
ull hr[2*N],hl[2*N],p[2*N];//超过ull自动取余
char s[N*2];
ull get_hash(ull h[],int l,int r)//获取某段长度字符串的hash值
{
    return h[r]-h[l-1]*p[r-l+1];
}
int main()
{
    int t=1;
    while(scanf("%s",s+1),strcmp(s+1,"END"))
    {
        int res=0;
        int n=strlen(s+1);
        for(int i=2*n;i>0;i-=2)
        {
            s[i]=s[i/2];
            s[i-1]='z'+1;//添加#(未出现字符)
        }
        n*=2,p[0]=1;
        for(int i=1,j=n;i<=n;i++,j--)
        {
            hl[i]=hl[i-1]*base+s[i]-'a'+1;//处理hash值
            hr[i]=hr[i-1]*base+s[j]-'a'+1;
            p[i]=p[i-1]*base;
        }
        for(int i=1;i<n;i++)
        {
            int l=0,r=min(i-1,n-i),mid;//二分寻找以str[i]为中心的最长回文子串的长度
            while(r>l)
            {
                mid=l+r+1>>1;
                if(get_hash(hl,i-mid,i-1)==get_hash(hr,n-(mid+i)+1,n-(i+1)+1))
                    l=mid;
                else r=mid-1;
            }
            if(s[i-l]=='z'+1)
               res=max(res,l);
            else res=max(res,l+1);
        }
        printf("Case %d: %d\n",t++,res);
    }
    return 0;
}

Manacher O ( n ) O(n) O(n)

参考视频链接
参考博客
例题链接

给定一个长度为 n 的由小写字母构成的字符串,求它的最长回文子串的长度是多少。

输入格式
一个由小写字母构成的字符串。

输出格式
输出一个整数,表示最长回文子串的长度。

数据范围
1≤n≤1e7
输入样例:
abcbabcbabcba
输出样例:
13
这道题字符串长度较大,明显是用O(n)时间复杂度的manacher算法,manacher算法具体过程可以看看上面的视频和博客,大佬介绍非常清楚,易于理解。我的理解是manacher算法是对DP和回文串对称性的利用。

代码如下:

#include<bits/stdc++.h>
using namespace std;
int manacher(string str)
{
    if(!str.length()) return 0;
    int l=(int)(str.length()*2+1);
    char *s=new char[l];//记录回文子串
    int *p=new int[l];//记录每个位置最大回文半径
    int r,c,index,mx;
    r=c=-1;
    index=mx=0;
    for(int i=0;i<l;i++) s[i]=i&1?str[index++]:'#';
    for(int i=0;i<l;i++)
    {
        p[i]=r>i?min(r - i, p[2 * c - i]):1;
        while(i+p[i]<l&&i-p[i]>-1)
        {
            if(s[i+p[i]]==s[i-p[i]]) p[i]++;
            else break;
        }
        if(i+p[i]>r) 
        {
            r=i+p[i];
            c=i;
        }
        mx=max(mx,p[i]);
    }
    delete[] s;
	delete[] p;
    return mx-1;
    
}
int main()
{
    string str;
    cin>>str;
    cout<<manacher(str)<<endl;
    return 0;
}

模板题:hdu3068上面的代码稍微改一下就能过。

  • 28
    点赞
  • 81
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

chp的博客

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值