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

目录

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

简介

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

讲解

例题

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

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

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

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

为什么在开头与结尾插入一个'#',首先,仔细思考:

在#a#a#a#a#不管以哪个字符为中心,枚举到的回文子串的开头与结尾都肯定是'#'号,如:a#a,我们可以在两个a再向外扩展一个'#'。

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

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

2. 统计答案

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

首先,我们定义一个数组ma数组,\(ma_{i}\)代表以\(i\)到以\(i\)为中心的最大回文子串的开头的长度,如:#a#b#b#c#b#,中以\(c\)为中心,组成的最大回文子串为#b#c#b#,c到最右边的#的串长度为4,所以他的\(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,我们可以发现,\(st_{2}=st_{4},st_{1}=st_{5}\),我们可以大胆的猜想一下我们可以将\(ma_{2}\)作为一个参考,\(ma_{2}\)值为2,仔细一看,\(ma_{4}\)的值至少为2!

继续参考EXKMP。

我们得到一下步骤:

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

其实在\(i+L-1>p 并且 i≤p\)时,ma值其实直接等于\(p-i+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^{2})\),就不大正常了,简单的例题都是\(O(n)\)可以统计的。

练习1

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

然后在后面在更新全局:\(ll_{i}=mymax(ll_{i},ll_{i-2}-2)\)(跳过'#'),\(rr_{i}=mymax(rr_{i},rr_{i+2}-2)\)(跳过'#')。

#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

Manacher(马拉车)学习笔记的更多相关文章

  1. Manacher算法学习笔记 | LeetCode#5

    Manacher算法学习笔记 DECLARATION 引用来源:https://www.cnblogs.com/grandyang/p/4475985.html CONTENT 用途:寻找一个字符串的 ...

  2. [Manacher]【学习笔记】

    终于填坑啦......马拉车 课件上说的好短,但是明白了,讲解稍微修改一下抄上行了,比扩展KMP好写多了 求以每个字符为中心的最长回文串的半径.如果要求可以以字符间隙为回文中心,就要在每两个字符之间及 ...

  3. Manacher算法学习笔记

    前言 Manacher(也叫马拉车)是一种用于在线性时间内找出字符串中最长回文子串的算法 算法 一般的查找回文串的算法是枚举中心,然后往两侧拓展,看最多拓展出多远.最坏情况下$O(n^2)$ 然而Ma ...

  4. Manacher 算法学习笔记

    算法用处: 解决最长回文子串的问题(朴素型). 算法复杂度 我们不妨先看看其他暴力解法的复杂度: \(O(n^3)\) 枚举子串的左右边界,然后再暴力判断是否回文,对答案取 \(max\) . \(O ...

  5. 学习笔记 - Manacher算法

    Manacher算法 - 学习笔记 是从最近Codeforces的一场比赛了解到这个算法的~ 非常新奇,毕竟是第一次听说 \(O(n)\) 的回文串算法 我在 vjudge 上开了一个[练习],有兴趣 ...

  6. 【学习笔记】字符串—马拉车(Manacher)

    [学习笔记]字符串-马拉车(Manacher) 一:[前言] 马拉车用于求解连续回文子串问题,效率极高. 其核心思想与 \(kmp\) 类似:继承. --引自 \(yyx\) 学姐 二:[算法原理] ...

  7. OI知识点|NOIP考点|省选考点|教程与学习笔记合集

    点亮技能树行动-- 本篇blog按照分类将网上写的OI知识点归纳了一下,然后会附上蒟蒻我的学习笔记或者是我认为写的不错的专题博客qwqwqwq(好吧,其实已经咕咕咕了...) 基础算法 贪心 枚举 分 ...

  8. js学习笔记:webpack基础入门(一)

    之前听说过webpack,今天想正式的接触一下,先跟着webpack的官方用户指南走: 在这里有: 如何安装webpack 如何使用webpack 如何使用loader 如何使用webpack的开发者 ...

  9. PHP-自定义模板-学习笔记

    1.  开始 这几天,看了李炎恢老师的<PHP第二季度视频>中的“章节7:创建TPL自定义模板”,做一个学习笔记,通过绘制架构图.UML类图和思维导图,来对加深理解. 2.  整体架构图 ...

  10. PHP-会员登录与注册例子解析-学习笔记

    1.开始 最近开始学习李炎恢老师的<PHP第二季度视频>中的“章节5:使用OOP注册会员”,做一个学习笔记,通过绘制基本页面流程和UML类图,来对加深理解. 2.基本页面流程 3.通过UM ...

随机推荐

  1. mysql 省市数据

    CREATE TABLE `province` ( `id` ) DEFAULT NULL, `name` ) DEFAULT NULL ) ENGINE=INNODB DEFAULT CHARSET ...

  2. 《浪潮之巅》(第2版):精彩的IT商战史

    2011年看过第一版.以为看新版会跳过大部分看过的内容,结果发现还是从头到尾看了一遍,2011年看过的内容已经记不太确切了:) 另外IT的历史太精彩了,许多故事都知道,再看还是挺有意思.当然作者的文字 ...

  3. makefile 编译指定目录

    makefile 编译指定目录 sub1=test1 sub2=test2 subs = sub1 sub2 SUBDIRS =$(foreach i, $(subs), $($(i))) .PHON ...

  4. C++学习之【使用位操作符求素数分析】

    先放普通代码: #include <iostream> using namespace std; void getPrime_1() { const int MAXN = 100; boo ...

  5. 设计模式——简单工厂模式(SimpleFactory Pattern)

    最近做项目总是遇到这样或者那样的问题,代码不够简洁,代码可扩展性不够好,耦合度又太高了,导致经常有种想去重构又无从下手的感觉. 无意间翻出了之前买的一本书<大话设计模式>读了一遍,受益匪浅 ...

  6. Android(java)学习笔记9:JDK5之后的Lock锁的概述和使用

    1. Lock锁的概述: java.util.concurrent.locks,接口Lock 首先Lock是一个接口,Lock实现提供了比使用synchronized方法 和 同步代码块更为广泛的锁定 ...

  7. ZJOI2019Day2余姚中学游记(4.23~4.26)

    前言 \(Day2\),又是一场噩梦. 前段时间去做了挺多十二省联考和\(HNOI2019\)的题目,还订正掉了\(Day1\)的\(T1\)和\(T2\)(\(T3\)动态\(DP\)完全不想订正啊 ...

  8. Catalan数列

    引入 今天听学长讲了卡特兰数列后对其有了更深的认识,在此完善了一下之前的博客加以总结. 首先用一个经典的例子来描述一下Catalan数列,我们有一个1~n的数列和一个大小为n的栈,我们有如下两种操作: ...

  9. ES6学习笔记(对象)

    1.属性的简洁表示法 const foo = 'bar'; const baz = {foo}; baz // {foo: "bar"} // 等同于 const baz = {f ...

  10. markdown的常用高级操作。

    符号 说明 对应编码(使用时去掉空格) 英文怎么说 & AND 符号 & amp; ampersand < 小于 & lt; little 大于 & gt; gr ...