Manacher 算法浅谈
前言杂谈
认识我的人都喜欢叫我马拉车,如今,马拉车来浅谈 Manacher 了(不就是某天打板子的时候打错了吗,不就是啪啪打脸了吗)。
首先大家需要知道,Manacher 不是很常考,但是也是一项必备的算法。当遇到回文串之类的问题时,别人辛辛苦苦打一堆哈希,你用 Manacher 算法两个并列的循环就搞定了。而且有些回文自动机 PAM 的题有时候可以用 Manacher,不一定要学那么厉害的算法。
这样说下来,Manacher 一下子感觉有些高大上了。
放假前最后一天,GM 问初三下来的学没学过 Manacher,我顿时瞪大了眼睛(虽然某些人反复强调马拉车来调侃我。。),但我也非常清楚,Manacher 作用不大,短小精悍,题库里面就有
GM说了下半期讲树链剖分和分块,但是我却自己写着 Manacher 的blog。。
作用与思想
假如给你一个字符串,让你求这个字符串里面最长的回文串,你通常会怎么求。没学过 Manacher 的多半都会
当然有些时候,Manacher 可能会伴随二分,DP 等其他知识,所以做题的时候不能光想着 Manacher,还要结合实际进行分析。
那么 Manacher 求出最长回文子串的思想是什么呢??
首先我们需要有一个基础的
而这种做法肯定是会被
我们上述做法的劣势就是,以 abacabad
中,回文串有 aba
,abacaba
,而 abacaba
的右端点最靠后,此时
维护这个区间肯定是有好处的。如果
首先可以证明
但是我们貌似还需要考虑到一个问题,假如 ababacabad
,我们的区间为 ababa
,如果
所以最终结论:
所以我们从
而回文串分为奇数长度和偶数长度,上文的做法均为奇数长度,对于偶数长度,我们的思路一致,但是回文串中点的定义是
其时间复杂度可能会发生争议,而我是从 wiki 上面学的 Manacher,所以这里大家可以去 wiki 得到时间复杂度的证明。
模板代码
模板代码如下:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1.1e7+5;
int d1[MAXN],d2[MAXN];
char s[MAXN];
int maxx=-1;
int len;
int main()
{
scanf("%s",s);
len=strlen(s);
for(int i=0,l=0,r=-1;i<len;i++)
{
int k=(i>r)?(1):(min(d1[l+r-i],r-i+1));
while(i-k>=0&&i+k<len&&s[i-k]==s[i+k]) k++;
d1[i]=k--;
maxx=max(maxx,d1[i]*2-1);
if(i+k>r) l=i-k,r=i+k;
}
for(int i=0,l=0,r=-1;i<len;i++)
{
int k=(i>r)?(0):(min(d2[l+r-i+1],r-i+1));
while(i-1-k>=0&&i+k<len&&s[i-1-k]==s[i+k]) k++;
d2[i]=k--;
maxx=max(maxx,d2[i]*2);
if(i+k>r) l=i-k-1,r=i+k;
}
printf("%d",maxx);
return 0;
}
习题
年前几天做了几道小题,现在我来分享一下成果(均为个人做法,可能和题解做法有不同)。
最长双回文串
老套板板题。
首先不难想到标记法。我们知道一个双回文串需要一个回文串的右端点和一个回文串的左端点并在一起,那么我们只需要求出每个回文串的左右端点,进行标记法,最后处理一下就可以了。
设
即:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e5+5;
int d1[MAXN],d2[MAXN];
char s[MAXN];
int dp[MAXN],f[MAXN];
int maxx=2;
int len;
int main()
{
scanf("%s",s);
len=strlen(s);
for(int i=0;i<len;i++) dp[i]=f[i]=1;
for(int i=0,l=0,r=-1;i<len;i++)
{
int k=(i>r)?(1):(min(d1[l+r-i],r-i+1));
while(i-k>=0&&i+k<len&&s[i-k]==s[i+k]) dp[i+k]=max(dp[i+k],k*2+1),f[i-k]=max(f[i-k],k*2+1),k++;
d1[i]=k--;
if(i+k>r) l=i-k,r=i+k;
}
for(int i=0,l=0,r=-1;i<len;i++)
{
int k=(i>r)?(0):(min(d2[l+r-i+1],r-i+1));
while(i-1-k>=0&&i+k<len&&s[i-1-k]==s[i+k]) dp[i+k]=max(dp[i+k],k*2+2),f[i-k-1]=max(f[i-k-1],k*2+2),k++;
d2[i]=k--;
maxx=max(maxx,d2[i]*2);
if(i+k>r) l=i-k-1,r=i+k;
}
for(int i=0;i<len-1;i++) maxx=max(maxx,dp[i]+f[i+1]);
cout<<maxx;
return 0;
}
可这是错误的但是可以通过原题数据的。。
为什么我强调自己的想法是错误的?
因为我写博客的时候突然把我的代码 Hack 掉了。
比如 cdcabaccabacdcabac
,正确的答案是 aba
,因此无法连接之前的 cdcabaccabacdc
,导致 cdcabaccabacdcaba
无法被统计。
所以我的想法仅供参考,但如果你想贺掉这道题,你可以用我的垃圾思路去交,最好还是自己想一下或者去看一下别人的正解。
写博客的我已因为 Hack myself 而哭晕在卧室。
绿绿与串串
乍一看可能看不出来这是一道 Manacher,但是因为有这样的翻转操作,我们也可以轻松想到回文串,然后进一步进行分析,这就很明显和 Manacher 扯不开关系。
本题的难点,就是该怎么判断一个串可以通过多次翻转得到目标串?
这个其实非常简单,以 qwqwq
为例,假设我们已经求出了 qwq
是一个答案,那么我们再将 qwq
设为目标串,根据观察,qw
可以通过一次翻转得到 qwq
,那么同样的,qw
可以通过两次翻转得到 qwqwq
。
想到这里,我们就可以定义一个
而判断长度大的
对于通过多次翻转的子串,我们可以用同样的方式,更换目标串,然后套用同样的式子进行判断。
而且不难发现,所有的翻转的回文串都是奇数长度,所以我们可以省略跑偶数长度的 Manacher,只需要奇数长度的 Manacher 就可以了。
重点代码如下:
for(int i=len-1;i>=0;i--)
{
if(d1[i]+(i-1)==len-1) vis[i]=1;
if(vis[d1[i]+i-1]&&i+1==d1[i]) vis[i]=1;
}
Casting Spells
Manacher 明显的板板题。
首先不难想到 Manacher 只用跑偶数长度的,人家题目概念写的清清楚楚,奇数长度连分都分不了。然后又不难分析出
接着就是判断一个偶数长度回文串是否满足条件。我们可以将这个回文串拆成
说得简单,做起来貌似就有问题了。
还是第一题讲的问题,我们直接按照 Manacher 的方式扩展,就会忽略掉中间的一些可能给最优解带来代价的回文子串。假如我们直接按照求出来的最大回文串进行
所以我们不能在处理过后用最长的结果进行判断,而是在 Manacher 的过程中进行处理。但是就会和第一题的题外Hack性质一样。所以这样的修改只能是得到更多的分数(或者说可能证明这样不会影响答案,反正我还没有进行 Hack myself)。
但是改之后我们可以想到,不用求出
老话:如果你想用错误代码贺一下原题,你可以按照我的思路来写。
for(int i=0,l=0,r=-1;i<len;i++)
{
int k=(i>r)?(0):(min(d2[l+r-i+1],r-i+1));
while(i-1-k>=0&&i+k<len&&s[i-1-k]==s[i+k])
{
if(!(k&1))
{
int id1=(i*2-k)>>1;
if(d2[id1]>=k>>1) maxx=max(maxx,k*2);
}
k++;
}
d2[i]=k--;
if(!(d2[i]&1))
{
int id1=(i*2-d2[i])>>1;
if(d2[id1]>=d2[i]>>1) maxx=max(maxx,d2[i]*2);
}
if(i+k>r) l=i-k-1,r=i+k;
}
「JSOI2016」扭动的回文串
然而是 yzh 告诉了我这道题然后我从上午大课间一直想到了晚上最后才发现有一个非常智慧的垃圾结论导致我心中无限个草泥马飞驰而过虽然最后还是打完了这道题。。
前面两个情况非常的板,就是分别在
第三种情况是本题的难点,我们不难想到,要讨论回文串中心是在
继而可以想到枚举这个中心
然而枚举每一个
证明:我们设另一个以
假设表格中第一列下标为 f
。但是如果我们选择 f
。因此两种情况是等价的,那我们直接选择
于是具体的思路就出来了:
-
用 Manacher 处理
中的最长回文串。 -
钦定扭动回文串的中点
在 上,找到以 为中心的最长回文串,左右端点为 。然后在这个串的前面和后面一起二分求出最长的公共子串,最后进行统计。 -
钦定中点
在 上,和上面的方法是一样的。
代码如下:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1e5 + 5;
const int base = 179;
int n, res;
char a[MAXN], b[MAXN];
int d1[MAXN], d2[MAXN];
unsigned long long Hash1[MAXN], Hash2[MAXN], mul[MAXN];
int main() {
cin >> n >> (a + 1) >> (b + 1);
mul[0] = 1;
for (int i = 1; i <= n; i++) mul[i] = mul[i - 1] * base;
for (int i = 1; i <= n; i++) Hash1[i] = Hash1[i - 1] * base + a[i];
for (int i = n; i >= 1; i--) Hash2[i] = Hash2[i + 1] * base + b[i];
for (int i = 1, l = 1, r = 0; i <= n; i++) {
int k = (i > r) ? (1) : (min(d1[l + r - i], r - i + 1));
while (i - k >= 1 && i + k <= n && a[i - k] == a[i + k]) k++;
d1[i] = k--, res = max(res, d1[i] * 2 - 1);
if (i + k > r)
l = i - k, r = i + k;
}
for (int i = 1, l = 1, r = 0; i <= n; i++) {
int k = (i > r) ? (0) : (min(d2[l + r - i + 1], r - i + 1));
while (i - 1 - k >= 1 && i + k <= n && a[i - 1 - k] == a[i + k]) k++;
d2[i] = k--, res = max(res, d2[i] * 2);
if (i + k > r)
l = i - k - 1, r = i + k;
}
for (int i = 1; i <= n; i++) {
int k = i + d1[i] - 2, j = i - d1[i] + 1;
int l = 1, r = min(j - 1, n - k), id = 0;
while (l <= r) {
int mid = (l + r) / 2;
if (Hash1[j - 1] - Hash1[j - mid - 1] * mul[mid] == Hash2[k + 1] - Hash2[k + mid + 1] * mul[mid])
id = mid, l = mid + 1;
else
r = mid - 1;
}
res = max(res, (d1[i] + id) * 2 - 1);
k = i + d2[i] - 2, j = i - d2[i];
l = 1, r = min(j - 1, n - k), id = 0;
while (l <= r) {
int mid = (l + r) / 2;
if (Hash1[j - 1] - Hash1[j - mid - 1] * mul[mid] == Hash2[k + 1] - Hash2[k + mid + 1] * mul[mid])
id = mid, l = mid + 1;
else
r = mid - 1;
}
res = max(res, (d2[i] + id) * 2);
}
memset(d1, 0, sizeof(d1)), memset(d2, 0, sizeof(d2));
for (int i = 1, l = 1, r = 0; i <= n; i++) {
int k = (i > r) ? (1) : (min(d1[l + r - i], r - i + 1));
while (i - k >= 1 && i + k <= n && b[i - k] == b[i + k]) k++;
d1[i] = k--, res = max(res, d1[i] * 2 - 1);
if (i + k > r)
l = i - k, r = i + k;
}
for (int i = 1, l = 1, r = 0; i <= n; i++) {
int k = (i > r) ? (0) : (min(d2[l + r - i + 1], r - i + 1));
while (i - 1 - k >= 1 && i + k <= n && b[i - 1 - k] == b[i + k]) k++;
d2[i] = k--, res = max(res, d2[i] * 2);
if (i + k > r)
l = i - k - 1, r = i + k;
}
for (int i = 1; i <= n; i++) {
int k = i + d1[i] - 1, j = i - d1[i] + 2;
int l = 1, r = min(j - 1, n - k), id = 0;
while (l <= r) {
int mid = (l + r) / 2;
if (Hash1[j - 1] - Hash1[j - mid - 1] * mul[mid] == Hash2[k + 1] - Hash2[k + mid + 1] * mul[mid])
id = mid, l = mid + 1;
else
r = mid - 1;
}
res = max(res, (d1[i] + id) * 2 - 1);
k = i + d2[i] - 1, j = i - d2[i] + 1;
l = 1, r = min(j - 1, n - k), id = 0;
while (l <= r) {
int mid = (l + r) / 2;
if (Hash1[j - 1] - Hash1[j - mid - 1] * mul[mid] == Hash2[k + 1] - Hash2[k + mid + 1] * mul[mid])
id = mid, l = mid + 1;
else
r = mid - 1;
}
res = max(res, (d2[i] + id) * 2);
}
cout << res;
return 0;
}
简单的字符串
神仙题,麻了。。。这道题我首先是想了很久想到了接近正解的思路,只差临门一脚,但是最后的某个处理我问了别人,貌似都讲不清楚改怎么处理。。啊因此只能看题解了。个人认为这个结论挺难证明的,这里就解释一下吧。。
PART Ⅰ 本质分析
首先看了题,貌似想不到和回文串有什么关系。我们先观察一下其中的规律,不难发现同构的含义:在两个环上各选择一个起点,然后按照指定顺序遍历每一个节点,最终各自得到一个序列,有一种选择起点的方法满足两个序列相同,那么就是同构。
根据题目,我们可以想到枚举中点
进一步想,如果
PART II 思维转换
定义前半段按照下标顺序形成的子段为
我们先考虑基础情况。可以发现对于每一个
所以我们就可以阴差阳错地想到了和正解息息相关的思路。(其实如果告诉你这是个回文串题,说不定可能会想得到,但是你不说,我打死想不到。。)
因此对于基础情况,我们直接判断
然后就拓展到其他情况。在第一部分,我们知道了本质应该是去给
假设我们将
同理我们可以知道
因此这种情况的
然后你就懂了这题应该怎么搞了。就是先枚举中点
PART III 结论
然后我不会判断。。。这就是我差的那个临门一脚。。。
先抛出结论吧:对于一个长度为偶数的
证明:
令
设
此时有
设
由于
-
若
,则很明显 是 的前缀,由于 ,进而得到 是 的一个周期,即 是 的一个前缀。 -
若
,手玩都可以知道 是 的一个周期。
综上,
由于
因为
那么我们判断一个
因此我们只需要维护当前的最长回文前缀,并且在 Manacher 的时候计算以某个端点结尾的最长回文串,这样判断的时候就比较容易了。
代码如下:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 5005;
int n;
int a[MAXN];
int stk[MAXN], cnt;
int d2[MAXN], f[MAXN];
vector<int> vec[MAXN];
unordered_map<int, int> mp;
int main() {
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i];
int res = 0;
for (int t = 2; t <= n; t++) {
cnt = 0;
int l = t - 1, r = t;
while (l >= 1 && r <= n) stk[++cnt] = a[l], stk[++cnt] = a[r], l--, r++;
memset(f, 0, sizeof(f)), memset(d2, 0, sizeof(d2));
for (int i = 1, l = 1, r = 0; i <= cnt; i++) {
int k = (i > r) ? (0) : (min(d2[l + r - i + 1], r - i + 1));
while (i - 1 - k >= 1 && i + k <= cnt && stk[i - 1 - k] == stk[i + k]) k++;
d2[i] = k--, f[i + d2[i] - 1] = max(f[i + d2[i] - 1], d2[i] * 2);
if (i + k > r)
l = i - k - 1, r = i + k;
}
for (int i = cnt; i >= 1; i--) f[i] = max(f[i], f[i + 1] - 2);
int maxx = 0;
for (int i = 2; i <= cnt; i += 2) {
if (f[i] == i)
maxx = i;
if (d2[(i + maxx) / 2 + 1] >= (i - maxx) / 2)
res++;
else if (d2[(i - f[i]) / 2 + 1] >= (i - f[i]) / 2)
res++;
}
}
cout << res;
return 0;
}
/*
16
1 2 1 1 2 2 2 2 2 2 2 2 1 1 2 1
*/
总结
反正,Manacher 过一道题确实不难,但是想要写出完全不会被 Hack myself 的代码确实挺难的(for me)。所以没事不要看我的代码,我自己都把自己 Hack 傻了。但是大致思路应该和正解偏差不大,只需要在实现上花点功夫改正就行了。
话说 Manacher 难题挺多的。
如果有兴趣,可以去看一下 回文自动机PAM,就是储存字符串中所有回文串的强强的自动机。洛谷板板题就是求以
目前是觉得 Manacher 上正式考场的几率不大,所以就当 反正以后会讲,我提前学了不是乱脱控打代码??不会的啦,AC 很蓝的啦)。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现