Manacher(马拉车)学习笔记

本文深入讲解了Manacher算法,一种高效解决回文子串问题的方法,可在O(n)时间内找到字符串中的所有回文子串。文章通过实例演示了算法的工作原理,包括如何处理偶数与奇数回文子串,并提供了代码实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Manacher可以有效的在 O ( n ) O(n) O(n)时间内解决一个字符串的回文子串的题目

目录

  1. 简介
  2. 讲解
  3. 推介
  4. 简单的练习
  5. 恐怖的练习QAQ
  6. 小结

简介

开头都说了,Manacher是目前解决回文子串的最有效的方法之一,可以在 O ( n ) O(n) O(n)时间内处理出以这个点为中心的最大回文子串长度。

讲解

例题

1. 将偶数的回文子串处理成奇数回文子串

在暴力处理回文子串的过程中,我们会把偶数与奇数的分成两个判断

相信大家一定会有这个疑问,那么,我们在开头、结尾与两两字符中间插入一个没有出现过的字符,如:’#’,举个栗子:aaaa做完处理为:#a#a#a#a#。

那么偶数的回文子串就是以’#'为中心的字符串。

为什么在开头与结尾插入一个’#’,首先,仔细思考:
在#a#a#a#a#不管以哪个字符为中心,枚举到的回文子串的开头与结尾都肯定是’#‘号,如:a#a,我们可以在两个a再向外扩展一个’#’。

如果不在开头与结尾加的话,就会出现开头结尾不是’#'的异类回文子串,如a#a。

那么,我们就可以愉快直接算长度为奇数的回文子串了。

2. 统计答案

由于后面的内容会十分的血腥,所以我还是先把简单的搬到前面。

首先,我们定义一个数组ma数组, m a i ma_{i} mai代表以 i i i到以 i i i为中心的最大回文子串的开头的长度,如:#a#b#b#c#b#,中以 c c c为中心,组成的最大回文子串为#b#c#b#,c到最右边的#的串长度为4,所以他的 m a ma ma就为4!

那么,如何统计答案?

如图:
在这里插入图片描述

回归正题,我们发现,b的ma值是4,同时真正的回文子串是3(去掉#号),难道就是ma值减1?

没错,就是,为什么?
我们继续思考:把右边的字母与左边的’#'号调转一下

如图:
在这里插入图片描述

首先,我们知道,目前我们是以字母作为中心而不是’#’, 那么,在回文子串中中心左边的子串就呈现这种情况:#?#?#?#…#(问号是字母),而这个子串的长度是中心的ma值减1,’#'的个数是?的个数加1。

而右边也是#?#?#?#…#(问号是字符),长度也一样,’#‘与?的个数也一样,那么把右边的字母与左边的’#‘号调转一下,我们会发现右边的子串全是’#’,而左边只剩下一个’#'号。

那么,根据ma数组的定义,以字母为中心的回文子串的长度为ma值减1。

那以’#'为中心的呢?
在这里插入图片描述

貌似也是ma值减1哟。

继续,如果以#为中心,那么回文子串中心的左边与右边的子串也都是#?#?#?,长度为ma值减1,同时#的个数与?的个数相同,把左边的’#‘与右边的’#‘交换,那么左边全都是?,而中心是个’#'号,所以也是ma值减1。

处理ma数组

没错,这也是最重要的!

学过EXKMP的话,这个应该是可以自己手推的。

在这里插入图片描述

现在处理以a为中心的回文子串,难道还要从1开始?

细心的同学发现了,由于第三个位置(’#’)的ma值为3,我们可以发现, s t 2 = s t 4 , s t 1 = s t 5 st_{2}=st_{4},st_{1}=st_{5} st2=st4,st1=st5,我们可以大胆的猜想一下我们可以将 m a 2 ma_{2} ma2作为一个参考, m a 2 ma_{2} ma2值为2,仔细一看, m a 4 ma_{4} ma4的值至少为2!

继续参考EXKMP。

我们得到一下步骤:

  1. 统计目前回文子串能到的最远位置§与是哪个中心(b)。
  2. L = m a b − ( i − b ) L=ma_{b-(i-b)} L=mab(ib)
  3. i + L − 1 &lt; p i+L-1&lt;p i+L1<p时,ma的值直接等于L
  4. i + L − 1 ≥ p i+L-1≥p i+L1p时,ma值为 p − i + 1 p-i+1 pi+1,然后开始直接暴力匹配,更新b与p。

其实在 i + L − 1 &gt; p 并 且 i ≤ p i+L-1&gt;p 并且 i≤p i+L1>pip时,ma值其实直接等于 p − i + 1 p-i+1 pi+1就可以了,不需要暴力匹配,跟EXKMP差不多。

至于更改了b与p为什么答案还一样,我就不一一赘述了,都跟EXKMP差不多。

现在都还不懂的话,看代码自行理解吧

#include<cstdio>
#include<cstring>
#define  N  23000000
using  namespace  std;
char  st[N],sst[11000000];
int  ma[N],n,ans;//定义
inline  int  mymax(int  x,int  y){return  x>y?x:y;}
void  Man()
{
    int  b=0,p=0;
    for(int  i=1;i<=n;i++)
    {
        int  L=ma[b-(i-b)];
        if(i+L-1<p)ma[i]=L;//情况1
        else
        {
            int  pp=p-i+1<1?1:p-i+1;//至少为1
            while(i-pp>=1  &&  i+pp<=n  &&  st[i-pp]==st[i+pp])pp++;//(i-pp+1)-1>=1,i+pp-1+1<=n
            ma[i]=pp;b=i;p=ma[b]+b-1;ans=mymax(ans,ma[i]-1);//更新
        }
    }
}
int  main()
{
    scanf("%s",sst+1);n=strlen(sst+1);
    for(int  i=1;i<=n;i++)st[i*2]=sst[i];
    st[n*2+1]='#';for(int  i=1;i<=n;i++)st[i*2-1]='#';//填'#'号
    n=n*2+1;Man();//匹配
    printf("%d\n",ans);
    return  0;
}

推介

在这个OJ上找视频,还可以,以前就是在这学的

例题很多

其实Manacher在学完EXKMP后,在学一点Manacher的概念,就可以手推了,毕竟我也是在历史课上手推的。(应该也归功于以前学过一次)。

总之也挺好理解的,就不一一赘述了。

简单的练习

基本上都是自己会做的。。。

Manacher一大部分的统计题都是 O ( n ) O(n) O(n)可以统计,一般到了 O ( n 2 ) O(n^{2}) O(n2),就不大正常了,简单的例题都是 O ( n ) O(n) O(n)可以统计的。

练习1

在Manacher匹配中,统计每个位置为开头或结尾的回文子串最长是多少。

然后在后面在更新全局: l l i = m y m a x ( l l i , l l i − 2 − 2 ) ll_{i}=mymax(ll_{i},ll_{i-2}-2) lli=mymax(lli,lli22)(跳过’#’), r r i = m y m a x ( r r i , r r i + 2 − 2 ) rr_{i}=mymax(rr_{i},rr_{i+2}-2) rri=mymax(rri,rri+22)(跳过’#’)。

#include<cstdio>
#include<cstring>
#include<cstdlib>
#define  N  210000
using  namespace  std;
char  st[N],stt[N];
int  ma[N],f[N],n,ll[N]/*为开头*/,rr[N]/*为结尾*/,ans;
inline  int  mymax(int  x,int  y){return  x>y?x:y;}
void  Man()
{
    int  b=0,p=0;
    for(int  i=1;i<=n;i++)
    {
        int  L=ma[b-(i-b)];
        if(i+L-1<p)ma[i]=L;
        else
        {
            int  pp=p-i+1<1?1:p-i+1;
            while(i-pp>=1  &&  i+pp<=n  &&  st[i-pp]==st[i+pp])pp++;
            ma[i]=pp;b=i;p=ma[b]+b-1;
        }
        ll[i-ma[i]+2]=ma[i]-1;
        !rr[i+ma[i]-2]?rr[i+ma[i]-2]=ma[i]-1:0;//用贪心来省时间
    }
}
int  main()
{
    scanf("%s",stt+1);n=strlen(stt+1);
    st[n*2+1]='#';for(int  i=1;i<=n;i++)st[i*2]=stt[i],st[i*2-1]='#';
    n=n*2+1;Man();
    for(int  i=2;i<n;i+=2)ll[i]=mymax(ll[i],ll[i-2]-2);
    for(int  i=n-1;i>=2;i-=2)rr[i]=mymax(rr[i],rr[i+2]-2);//后面递推更新
    for(int  i=4;i<=n;i+=2)
    {
        if(rr[i-2]  &&  ll[i])ans=mymax(ans,rr[i-2]+ll[i]);//统计答案
    }
    printf("%d\n",ans);//输出
    return  0;
}

练习2

用类似差分统计,然后用快速幂加速一下。

#include<cstdio>
#include<cstring>
#define  N  1100000
#define  mod  19930726
using  namespace  std;
typedef  long  long  ll;
char  st[N];
int  ma[N],n;
ll  sum[N],ans,f[N],k;
void  Man()
{
    f[1]=n;
    int  b=0,p=0;
    for(int  i=1;i<=n;i++)
    {
        int  L=ma[b-(i-b)];
        if(i+L-1<p)ma[i]=L;
        else
        {
            int  pp=p-i+1<1?1:p-i+1;
            while(i-pp>=1  &&  i+pp<=n  &&  st[i-pp]==st[i+pp])pp++;
            ma[i]=pp;b=i;p=ma[b]+b-1;
        }
        f[ma[i]*2+1]--;
    }
}
ll  kpow(ll  x,ll  p)//快速幂
{
    ll  qwq=1;
    while(p)
    {
        if(p%2==1)qwq*=x,qwq%=mod;
        x*=x;p>>=1;x%=mod;
    }
    return  qwq;
}
int  main()
{
    scanf("%d%lld",&n,&k);
    scanf("%s",st+1);
    Man();
    for(int  i=1;i<=n;i+=2)f[i]+=f[i-2];//差分的前缀和。
    ans=1;
    for(int  i=n;i>=1;i--)
    {
    	if((i&1)==0)continue;//不是'#'
        sum[i]=f[i]+sum[i+2];
        if(sum[i]>=k)//达到限制
        {
            ans*=kpow(i,k-sum[i+2]);ans%=mod;
            break;//退出
        }
        if(i==1)//没有
        {
            printf("-1\n");
            return  0;
        }
        ans*=kpow(i,f[i]);ans%=mod;
    }
    printf("%lld\n",ans);//输出
    return  0;
}

练习三

同样记录是否是开头或结尾,然后用乘法统计,一个数字乘以一个前缀和而已。

当然还是用了差分。。。

就不多讲了,luogu有题解

//用了与第一题不同的统计方法
#include<cstdio>
#include<cstring>
#define  N  4100
using  namespace  std;
char  st[N],stt[N];
int  ma[N],n,ll[N],rr[N];
long  long  ans;
void  Man()
{
    int  b=0,p=0;
    for(int  i=1;i<=n;i++)
    {
        int  L=ma[b-(i-b)];
        if(i+L-1<p)ma[i]=L;
        else
        {
            int  pp=p-i+1<1?1:p-i+1;
            while(i-pp>=1  &&  i+pp<=n  &&  st[i-pp]==st[i+pp])pp++;
            ma[i]=pp;b=i;p=b+ma[b]-1;
        }
        ll[i-ma[i]+1]++;ll[i]--;rr[i+ma[i]-1]++;rr[i]--;//差分
    }
}
int  main()
{
    scanf("%s",stt+1);n=strlen(stt+1);
    st[1]='#';for(int  i=1;i<=n;i++)st[i*2+1]='#',st[i*2]=stt[i];
    n=n*2+1;Man();//匹配
    for(int  i=1;i<=n;i++)ll[i]+=ll[i-1];
    for(int  i=n;i>=1;i--)rr[i]+=rr[i+1];//先处理每个数的和
    for(int  i=3;i<=n;i+=2)rr[i]+=rr[i-2];//再处理一个的前缀和
    for(int  i=1;i<=n;i+=2)ans+=ll[i]*rr[i];//统计
    printf("%lld\n",ans);
    return  0;
}

练习四

这里稍微有些不同,我们不是找两边相等的回文子串,而是找两边相反的回文子串,如:1100。

而且必须是以’#‘为中心,首先,按题意理解,回文子串必须是偶数长度的,其二,由于换了回文子串的定义,所以在Manacher的匹配中,以一个数字为中心也会出错,这个很容易想为什么,所以Manacher只找以’#'为中心的情况就是了。

#include<cstdio>
#include<cstring>
#define  N  1100000
using  namespace  std;
char  st[N],stt[N];
int  ma[N],n;
long  long  ans;
void  Man()
{
    int  b=0,p=0;
    for(int  i=1;i<=n;i+=2/*只找'#'号*/)
    {
        int  L=ma[b-(i-b)];
        if(i+L-1<p)ma[i]=L;
        else
        {
            int  pp=p-i+1<1?1:p-i+1;
            while(i-pp>=1  &&  i+pp<=n  &&  (st[i-pp]=='#'?1:st[i-pp]==(st[i+pp]^1)/*不同的计算方法*/))pp++;
            ma[i]=pp;b=i;p=ma[b]+b-1;
        }
        ans+=(ma[i]-1)/2;//统计
    }
}
int  main()
{
    scanf("%d",&n);
    scanf("%s",stt+1);
    st[1]='#';for(int  i=1;i<=n;i++)st[i*2+1]='#',st[i*2]=stt[i];
    n=n*2+1;
    Man();
    printf("%lld\n",ans);
	return  0;
}

练习五

用一个数组记录以这个位置为开头的最长不下降子序列的长度。

然后匹配的时候统计答案。

#include<cstdio>
#include<cstring>
#define  N  210000
using  namespace  std;
int  n,st[N],ma[N],f[N],ans;
inline  int  mymin(int  x,int  y){return  x<y?x:y;}//最小值
inline  int  mymax(int  x,int  y){return  x>y?x:y;}//最大值
void  Man()
{
    int  b=0,p=0;
    for(int  i=1;i<=n;i++)
    {
        int  L=ma[b-(i-b)];
        if(i+L-1<p)ma[i]=L;
        else
        {
            int  pp=p-i+1<1?1:p-i+1;
            while(i-pp>=1  &&  i+pp<=n  &&  st[i-pp]==st[i+pp])pp++;
            ma[i]=pp;b=i;p=ma[b]+b-1;
        }
        ans=mymax(mymin(ma[i]-1,f[i]-1),ans);//统计
    }
}
int  main()
{
    int  T;scanf("%d",&T);
    while(T--)
    {
        ans=0;
        scanf("%d",&n);
        st[1]=-1;
        for(int  i=1;i<=n;i++)
        {
            scanf("%d",&st[i*2]);
            st[i*2+1]=-1;
        }
        n=n*2+1;
        //输入
        f[1]=1;
        int  mind=251,minid=0;
        for(int  i=2;i<=n;i+=2)
        {
            if(mind<=st[i])mind=st[i],f[i]=i-minid+2;//包括一个-1
            else  mind=st[i],minid=i,f[i]=2;
            f[i+1]=f[i]+1;//方便后面记录答案
        }//处理最长不下降子序列
        Man();
        printf("%d\n",ans);
    }
    return  0;
}

恐怖的练习

练习一

听说是Hash+Manacher判重,不过不想做了QAQ。

练习二

这个就真的不会了QAQ,好像是Manacher+朴素统计+优化。

小结

Manacher真的是一个不错的算法QMQ

资源下载链接为: https://pan.quark.cn/s/f989b9092fc5 HttpServletRequestWrapper 是 Java Servlet API 中的一个工具类,位于 javax.servlet.http 包中,用于对 HttpServletRequest 对象进行封装,从而在 Web 应用中实现对 HTTP 请求的拦截、修改或增强等功能。通过继承该类并覆盖相关方法,开发者可以轻松地自定义请求处理逻辑,例如修改请求参数、添加请求头、记录日志等。 参数过滤:在请求到达处理器之前,可以对请求参数进行检查或修改,例如去除 URL 编码、过滤敏感信息或进行安全检查。 请求头操作:可以修改或添加请求头,比如设置自定义的 Content-Type 或添加认证信息。 请求属性扩展:在原始请求的基础上添加自定义属性,供后续处理使用。 日志记录:在处理请求前记录请求信息,如 URL、参数、请求头等,便于调试和监控。 跨域支持:通过添加 CORS 相关的响应头,允许来自不同源的请求。 HttpServletRequestWrapper 通过继承 HttpServletRequest 接口并重写其方法来实现功能。开发者可以在重写的方法中添加自定义逻辑,例如在获取参数时进行过滤,或在读取请求体时进行解密。当调用这些方法时,实际上是调用了包装器中的方法,从而实现了对原始请求的修改或增强。 以下是一个简单的示例,展示如何创建一个用于过滤请求参数的包装器: 在 doFilter 方法中,可以使用 CustomRequestWrapper 包装原始请求: 这样,每当调用 getParameterValues 方法时,都会先经过自定义的过滤逻辑。 HttpServletRequestWrapper 是 Java Web 开发中一个强大的工具,它提供了灵活的扩展性,允许开发者
### 实现与解释马拉算法 #### 一、算法概述 马拉算法(Manacher's Algorithm)用于在线性时间内查找字符串中的最长回文子串。该算法通过巧妙地处理奇偶长度的回文问题,使得整个过程可以在 O(n) 时间复杂度下完成[^1]。 #### 二、算法过程分析 ##### 1. 字符串预处理 为了统一处理不同长度的回文情况,通常会在原字符串之间插入特殊字符 `#` ,并添加头尾两个不同的保护字符 `$` 和 `@` 。这样做的目的是让所有的回文中心都有一个明确的位置,并且不会越界访问[^2]。 例如:"abba" 被转换成 "$#a#b#b#a#@" ##### 2. 原字符串与新字符串的关联 经过上述变换后的字符串称为辅助串 S' , 对应位置 i 处的最大半径 P[i] 表示以第i个字符为中心能扩展出去最远距离的一半加一 (即实际回文长度除以2再加1)[^3]。 ##### 3. 利用已知信息加速计算 在遍历过程中维护三个变量:当前最大右端点 id, 当前最大右侧边界 mr 及其对应的中心 pos;当遇到新的待求解点 j 时分两种情况进行讨论: ###### 3.1 维护数据 如果j位于mr之内,则尝试借用pos处的信息来减少不必要的比较次数;否则直接从零开始匹配直到无法继续为止。 ###### 3.2 初始化规则 根据 j 是否处于已有记录的有效区间内采取不同策略初始化 p[j]: - **若 j 属于有效区域** :设 k=2*pos-j 即关于pos对称的那个索引位上的值作为初始估计值 min(p[k], mr-j+1), 这样既考虑到了镜像关系又防止超出范围; - **反之则置为默认最小值1**, 因为我们至少知道它自己本身构成的一个单字符回文. ###### 3.3 探测阶段 无论哪种情形都需要进一步验证是否存在更长的可能性,具体做法是从p[j]-1往两侧逐次试探直至不满足条件停止更新p[j][^3]。 最后每当发现更大的mr就及时刷新id和pos以便后续节点能够受益于此优化措施. ```python def manacher(s): # Preprocess the string to handle even-length palindromes uniformly. t = '#'.join(f'^{s}$') n = len(t) p = [0]*n center = right = 0 max_len = 0 index = 0 for i in range(1,n-1): mirror = 2 * center - i if right > i: p[i] = min(right-i,p[mirror]) while t[i+(1+p[i])] == t[i-(1+p[i])]: p[i]+=1 if i + p[i]>right: center=i right=center+p[i] if p[i]>max_len: max_len=p[i] index=i start=(index-max_len)//2 return s[start:start+max_len] ``` 此代码实现了完整的马拉算法逻辑,包括必要的预处理步骤以及核心循环结构,最终返回输入字符串中最长连续回文子序列的内容。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值