manacher算法
manacher算法#
manacher(马拉车)算法是用于快速查找最长回文子串的算法。
基本思想是在遍历查找的基础上维护一个当前回文字符串的位置,用于加速后续的查找。
记,left
表示当前回文字符串的左边界;right
表示当前回文字符串的右边界;idx
表示当前索引的位置;mirror
表示索引在回文字符串中关于中心的镜像点。后面的当前记录的回文字符串使用[left,right]
表示。
那么可能会出现以下几种情况:
idx
超出了[left,right]
范围,也就是idx
>right
,这时没有更好的办法,只能遍历查找以当前位置为中心的最长回字符串,然后更新[left,right]
。idx
落在[left,right]
范围内,那么去查看mirror
的状态,因为mirror
肯定在idx
左侧的,也就是说在搜索idx
位置的回文字符串时,mirror
一定是被搜索和计算过了的。又因为落在了[left,right]
中,意味着mirror+1
位置和idx-1
位置的元素相同;如果mirror-1
位置还落在区间中,那么mirror-1
位置和idx+1
位置的元素也相同,可以得到这么一条结论:在[left,right]
区间内,idx
位置对应的回文字符串,至少拥有和mirror
相同的长度。这时会再出现两种情况:
2.1.mirror
位置的回文字符串落在[left,right]
范围内(不包含压线的情况),那么idx
位置的回文字符串也不会超出范围,且和mirror
位置的长度相同,可以使用反证法来验证——mirror
回文字符串半径记作radius
,可以知道mirror+radius+1
肯定和mirror-radius-1
处的元素不同,假设idx
位置的回文字符串长度大于mirror
,则idx+radius+1
处的元素一定和idx-radius-1
处的相同,又因为落在[left,right]
范围内,则idx+radius+1
和mirror-radius-1
处的元素相同,且idx-radius-1
和mirror+radius+1
处的元素相同,这与已知的矛盾。
2.2.mirror
位置的回文字符串超出了[left,right]
范围(包含压线的情况),那么可以说,idx
至少到right
位置都是回文的,只需要从right
再向后遍历,并更新[left,right]
即可。
std::string manacher(std::string_view s) {
std::string tmp;
// 将字符串扩展成奇数 abcd -> #a#b#c#d#...
// 使得原字符串无论奇数还是偶数长度,都会变成奇数长度
for(int i = 0; i < static_cast<int>(s.size()); i++) {
tmp.push_back('#');
tmp.push_back(s[i]);
}
tmp.push_back('#');
// 定义回文字符串的范围,定义了当前的回文字符串的两端
int left = 0, right = 0;
// 定义了字符串中每个位置作为中心时的最大回文字符串半径
std::vector<int> radius(tmp.size(), 0);
for(int idx = 0; idx < static_cast<int>(tmp.size()); idx++) {
// 当搜索的位置超出了当前回文字符串的范围时,只能向后遍历
if(idx > right) {
int l = idx, r = idx;
while(l >= 0 && r < static_cast<int>(tmp.size()) &&
tmp[l] == tmp[r]) {
--l;
++r;
}
radius[idx] = (r - l - 2) / 2;
left = l + 1;
right = r - 1;
}
else { // 如果当前搜索的位置在当前的回文字符串范围中
// 搜索位置在当前回文字符串中关于中心的镜像点对应的回文字符串不超出当前回文字符串,说明当前搜索位置的回文字符串半径也只能这么长
int center= left + (right - left) / 2;
int mirror = center- (idx - center);
if(radius[mirror] < mirror - left) {
radius[idx] = radius[mirror];
}
else { // 如果镜像点的长度超出了当前回文字符串,则当前搜索位置还需要继续向后遍历
int l = idx - (mirror - left), r = idx + (mirror - left);
while(l >= 0 && r < static_cast<int>(tmp.size()) &&
tmp[l] == tmp[r]) {
--l;
++r;
}
radius[idx] = (r - l - 2) / 2;
left = l + 1;
right = r - 1;
}
}
}
// 找到最长的回文字符串
int pos = 0;
for(int i = 0; i < static_cast<int>(radius.size()); i++) {
pos = radius[i] > radius[pos] ? i : pos;
}
// 此时,pos对应了扩展后的字符串中最长回文字符串的中心,通过它再映射回原字符串
std::string res;
for(int i = pos - radius[pos]; i <= pos + radius[pos]; i++) {
if(tmp[i] == '#') {
continue;
}
res.push_back(tmp[i]);
}
return res;
}
原字符串长度可能是奇数或者偶数,可以使用技巧——相邻元素间插入不可能出现的字符,将其都变成奇数长度的字符串,这样便于统一处理。不使用技巧的话也能处理,只不过就需要分奇偶长度了,但过程差不多。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· Apache Tomcat RCE漏洞复现(CVE-2025-24813)