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;
}
原字符串长度可能是奇数或者偶数,可以使用技巧——相邻元素间插入不可能出现的字符,将其都变成奇数长度的字符串,这样便于统一处理。不使用技巧的话也能处理,只不过就需要分奇偶长度了,但过程差不多。

浙公网安备 33010602011771号