Manacher算法

Manacher 算法可在 O(n) 解决最长回文串的问题。

通过预处理 di 表示以 i 为中心的回文串向两边延伸的最长长度来解决原问题。

如对于字符串 abcba(下标从1开始),d3=3,即 cba

P3805【模板】manacher

算法流程如下:

改造字符串

在开头插入 $,末尾插入 !,每个字符的左边和右边都插入一个 #

例如:

abcdefg
改造为:
$#a#b#c#d#e#f#g#!

在开头和末尾插入两个不同的字符,可以避免边界问题。而在相邻字符插入 #,则是为了处理长度为偶数的回文串,例如:aa 转化为 #a#a#,即变为以第二个 # 为中心的回文串。

预处理 di

在预处理 di 时,需要维护一个表示当前右端点最右边的回文串区间,不妨用 [l,r] 表示。

假设现在正在处理 i,那么分为两种情况:

i[l,r]
对于 i[l,r] 中,可以找到一些性质,记 i 表示 i 关于 l+r2 的对称点(注意:此时 di 已经求出),那么分为以下几种情况:

i 的回文长度未超出 [l,r](下图蓝色为回文部分)
由于 [l,r] 本身为回文串,所以在对称位置,回文情况是一致的,那么直接 di=di 即可。

i 的回文长度超出 [l,r]
那么我们只能确认图中(即在[l,r]内的部分)蓝色部分为回文,至于红色部分(超出区间部分),我们无法确认,只能通过暴力枚举处理。

i>r
直接暴力往两边扩展即可。

计算完 di 记得更新区间。

代码:

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=3e7;
int n,d[N];
string s;
char ch[N];
int main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin>>s;
ch[0]='$';//改造字符串
for(int i=0;i<s.size();i++) {
ch[++n]='#';
ch[++n]=s[i];
}
ch[++n]='#';
ch[++n]='!';
d[1]=1;
for(int l=1,r=1,i=2;i<n;i++) {
if(i>=l&&i<=r) d[i]=min(d[l+r-i],r-i+1);//l+r-i即为i',r-i+1即i到右边界距离
while(ch[i+d[i]]==ch[i-d[i]]) d[i]++;//暴力扩展
if(i+d[i]-1>=r) {r=i+d[i]-1; l=i-d[i]+1;}//更新区间
}
int ans=0;
for(int i=1;i<=n;i++) {
ans=max(ans,d[i]-1);
}
cout<<ans;
return 0;
}

扩展应用

裸的Manacher算法只能求出以一个字符为中心向两边延伸的最长回文长度,但是没法求出以某个点为起点的最长回文长度,需要通过递推的方式求解。

首先预处理出上文的 d 数组,然后用 idi 计算出 [li,ri],此时计算出的所有区间,都是尽可能长的区间,也就是说:如果 [1,5]是回文,那么[2,4]也一定是回文,而目前的做法只能处理出 [1,5],处理不出 [2,4]

所以考虑用 dp 解决问题,设 fi 表示以 i 为开头的最长长度,先用 i,di 计算出一部分 fi

从上文 [1,5][2.4] 的例子也能得到一些启发,即前一个开头的最长回文损失两个(一头一尾)就变成当前位置回文长度,即:

fifi12

当然,在 Manacher 算法中,由于字符之间用 # 隔开,所以实际改写为:

fifi22

类似例题:P4555 [国家集训队] 最长双回文串
提交记录:记录详情

posted @   2017BeiJiang  阅读(3)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· NetPad:一个.NET开源、跨平台的C#编辑器
· PowerShell开发游戏 · 打蜜蜂
· 凌晨三点救火实录:Java内存泄漏的七个神坑,你至少踩过三个!
点击右上角即可分享
微信分享提示