Manacher
1 问题引入
给定一个长度为
显然对于一个长度为
那如何优化复杂度呢?这就要提到 Manacher 算法了。在探讨这个算法之前,我们需要先了解其基本的描述回文串的思路。
2 基本思想及暴力算法
2.1 基本思想
考虑对于回文串
例如在字符串 abababc
中,b
,aba
,babab
三个。根据这个例子我们也可以看出,两个数组其实也表示以
那么如果我们有了
似乎这两个数组的求解也并不容易,于是接下来的问题就是怎样快速求出这两个数组。
2.2 暴力算法
显然在看到上面的定义后我们会立刻想到这样一种朴素的思路:对于每个
显然这个算法的时间复杂度为
3 Manacher 算法
3.1 算法过程
我们只以寻找奇数长度回文串为例说明,即只考虑数组
我们首先需要维护出一个最靠右的回文串的两个边界
现在我们要计算
-
如果
不在当前回文串之内,即 ,直接调用暴力算法求解。 -
如果
在当前回文串之内,即 :
考虑在当前回文串中
但是并不是每一种情况都可以这样做。如果以
最后需要注意的是,每次计算完
容易发现上面的算法其实多次运行了暴力算法,所以这个算法的时间复杂度并不直观。考虑这样的分析:每一次运行暴力算法都会使
当然我们上面只讲述了对于
考虑在原字符串中的每两个字符间(以及开头前和结尾后)加入一个其他字符,例如 abaab
变为 #a#b#a#a#b#
,这样我们会发现,原先不管长为奇数的回文串(aba
)还是长为偶数的回文串(baab
),最后都变成了长为奇数的回文串(a#b#a
和 b#a#a#b
)。于是在新字符串上我们只需要处理出一个
值得注意的是,原先回文串 S
在新字符串中会多计算一次 #S#
,因此最后应将答案除以
3.2 应用
上面讲解的求解回文子串个数是 Manacher 的一个应用,而另一个经典应用就是求最长回文子串。
实际上这个问题我们并不陌生,在之前的哈希以及后缀数组的学习中都遇到过。那时我们采用的都是二分,复杂度为
现在考虑对于每一个位置为对称中心的回文子串长度。当
因此无论是什么情况,最长回文子串长度就是
下面以 【模板】manacher 为例,给出代码:
#include <bits/stdc++.h>
using namespace std;
const int Maxn = 3e7 + 5;
const int Inf = 2e9;
string s;
int n;
string init(string s) {//给字符串中间加上 `#`
string t = " #";
for(int i = 0; i < s.size(); i++) {
(t += s[i]) += '#';
}
return t;
}
int d[Maxn];
void manacher(string s) {
int l = 1, r = 0;//初始化
for(int i = 1; i <= n; i++) {
int k = (i > r) ? 1 : min(d[l + r - i], r - i + 1);//这句话浓缩了上面讨论的三种情况
//k 代表的是当前以 i 为中心的最大回文串长度
while(s[i - k] == s[i + k]) k++;//暴力向外扩展
d[i] = k--;//记录答案
//注意这里减一是为了减去跳出 while 多出来的字符
if(i + k > r) {//更新 l,r
l = i - k, r = i + k;
}
}
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> s;
s = init(s);
n = s.size() - 1;
manacher(s);
int ans = 0;
for(int i = 1; i <= n; i++) {
ans = max(ans, d[i]);
}
cout << ans - 1;//注意最后要减一
return 0;
}
实际上,尽管上文中我们一直在讨论回文串。但实际上,只要能够满足类似于回文串的扩展性质,就能够使用 Manacher 进行求解。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律