算法 | Manacher
Manacher (manachar, 马拉车):在
回文半径
先介绍一个概念——回文半径,它是回文串的中心(“对称轴”)到回文串两端的长度。
它分为两类,一种是奇回文半径和偶回文半径,分别表示长度为奇数的回文串半径和长度为偶数的回文串半径。之所以与长度的奇偶性有关系,是因为长度为奇数时,回文串的“对称轴”恰好落在一个字符上,而长度为偶数时,“对称轴”落在字符之间。这两种情况在计算时有细节差异,因此得分开考虑。
例如对于奇数长度的回文串,回文半径是:
而对于偶数长度的回文串,就有所差别:
下面一个示例给出了奇回文半径和偶回文半径的差异:
统一化
上面可以看到,回文串的长度奇偶性会造成许多细节差异,导致代码实现非常麻烦,接下来介绍将这两种情况统一的方法。
对于一个长度为
awawa
#a#w#a#w#a#
这样,就向其中插入了
为了实现方便,字符串下标从
#a#w#a#w#a#
^#a#w#a#w#a#$
这样,奇数长度和偶数长度的字符串都统一成奇数长度了。如果想要恢复原串,再把插入的过滤掉就行了。
string pre_process(string &s)
{
string ans = "^";
for (auto &c : s)
{
ans += '#';
ans += c;
}
ans += '#';
ans += '$';
return ans;
}
综上,下面的介绍都只介绍计算奇数长度的方法,需要先进行预处理统一后再使用。
朴素算法(中心扩展法)
在介绍马拉车算法前,先介绍解决上面这个问题的朴素算法,即中心扩展法。
中心扩展法的流程是:
对字符串内每一位字符
,进行下面的操作:- 以
为中心,向左右进行扩展,直到发现 或到达边缘则停止。 - 记该字符的回文半径
.
- 以
这个算法的时间复杂度显然为
manacher 算法
manacher 算法的核心思想就是充分利用已经计算过的部分推导出还没计算的部分,避免重复计算。
manacher 算法计算一位的回文半径有三种方法,在三种不同情况下使用:
- 直接对称
- 对称 + 中心扩展
- 直接中心扩展
直接对称
假设目前已经计算得到一个大回文串,且大回文串左侧的回文半径已经计算出了了。如果左侧某个位置的回文半径对应的回文串与大回文串的边界不重合,那么这个位置对称到右侧的回文半径与左侧相等。
用形式化的说法,如果目前已知的边界最靠右的回文串为
这种情况的示意图如下:
使用这个方法,可以直接令
为什么不令
注意,在每次计算
对称 + 中心扩展
假设目前已经计算得到一个大回文串,且大回文串左侧的回文半径已经计算出了了。如果左侧某个位置的回文半径对应的回文串与大回文串的边界重合,那么先将这个位置的回文半径取为到大回文串边界的距离,再用中心扩展法尝试扩展。
用形式化的说法,如果目前已知的边界最靠右的回文串为
这种情况的示意图如下:
为什么这种情况不能直接对称过来赋值呢?其实上面这个示意图已经展现出来了,如果将右侧这个赋值为
使用这个方法,可以直接令
注意,在每次计算
直接中心扩展
对于没有包含在大回文串里的部分,那就只能用中心扩展法了。对应的情况示例如下:
注意,在每次计算
代码实现
前面说那么复杂,其实代码很简单,三种情况分类讨论其实用一个三元表达式就搞定了。代码内 r 代表当前边界最靠右的回文串右边界,mid 是当前边界最靠右的回文串的中心。
// s - 字符串(下标1开始,需要预处理)
// p - 对应位置回文半径
void manacher(string &s, vector<int> &p)
{
int r = 0, mid = 0;
for (int i = 1; i < s.size() - 1; i++)
{
p[i] = r > i ? min(p[2 * mid - i], r - i) : 1;
while (s[i + p[i]] == s[i - p[i]])
p[i]++;
if (i + p[i] > r)
{
r = i + p[i];
mid = i;
}
}
}
本文采用 CC BY-SA 4.0 许可,本文 Markdown 源码:Haotian-BiJi