Manacher算法
文章默认给定字符串中只会出现小写英文字母
介绍
通过已经学习了的 字符串哈希 ,我们可以用 \(O(n\log n)\) 的时间复杂度求解一个串中的最长回文子串了,那么我们思考一下是否用字符串哈希在线性的时间内完成这个问题呢?
当然可以!但是具体做法我们不会在此介绍,感兴趣可以看 OI Wiki 中的做法。
在求解回文串问题的时候,我们会更常使用到一个算法,它能在线性时间内求出该串中全部位置的最长回文半径,这个算法称为 \(Manacher\) 算法。
预处理
在求解一个位置的回文半径时,显然需要该串的长度为奇数,我们可以直接选取该位置作为中心,然后向两侧扩展即可。例如:
字符串 | a | b | a |
---|---|---|---|
索引 | 1 | 2 | 3 |
回文半径 | 1 | 2 | 1 |
这个串的每个位置的回文半径显然是不难求得的,但是如果是这种情况:
字符串 | a | b | b | a |
---|---|---|---|---|
索引 | 1 | 2 | 3 | 4 |
回文半径 | 1 | 1 | 1 | 1 |
如果按照所求出的解,该串应该不含长度大于 \(1\) 的回文子串,但显然这与事实相悖,因此为了得到一个正确的解,我们需要对该字符串进行加工处理。
具体处理的方法是在原有串的字符之间加入一些不属于该串的字符,以串 \(abba\) 举例,我们可以将其变为 \(|a|b|b|a|\) ,串的长度从原来的 \(n\) 变为了现在的 \(2n+1\) 在处理之后我们可以再观察一下每个位置的回文半径:
字符串 | | | a | | | b | | | b | | | a | | |
---|---|---|---|---|---|---|---|---|---|
索引 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
回文半径 | 1 | 2 | 1 | 2 | 5 | 2 | 1 | 2 | 1 |
虽然处理后的字符串可以全部转换为奇数长度的情况求解回文半径,但是处理后的回文半径显然与未处理的字符串有些差别,通过简单的观察,我们可以发现在进行字符匹配的时候, \('|'\) 只会和 \('|'\) 匹配,同样的字母也只会和字母匹配。
下面记位置 \(i\) 的回文半径为 \(p_i\)
-
以 \('|'\) 为中心的位置对应的 \(p_i\) 一定为奇数。因为匹配的起点是 \('|'\) ,且终点也为 \('|'\) 。除了中心以外,字母和 \('|'\) 都是成对出现的。
-
以字母为中心的位置对应的 \(p_i\) 一定为偶数。因为匹配的起点是字母 ,且终点为 \('|'\) 。全部字母和 \('|'\) 都是成对出现的。
不难发现,在处理完字符串中全部位置所对应的 \(p_i\) 后,\(p_i-1\) 就是对应原字符串中最长的回文串长度
。
优化
在知道 \(p\) 数组的含义以后,我们要做的就是以 \(O(n)\) 的时间复杂度来求解每一个位置所对应的 \(p_i\) 。
在实现该算法之前,我们需要先定义两个变量 \(mid\) 和 \(R\) ,且初值可以均赋为 \(1\) ,其意义和作用会在下面提到。
计算 \(p_i\) 的过程是自左向右的,假设我们当前在求第 \(i\) 个位置的 \(p_i\) ,那么 \(p_1,p_2...p_{i-1}\) 是已经被计算完成了的。在此时我们需要动态维护一个区间使得 \(R\) 最大,其中:
\(mid\) 是小于 \(i\) 的一个位置,我们需要在完成一个位置的 \(p_i\) 的统计后及时更新 \(R\) 的值。
那么我们通过图片来理解一下
此刻有很多种情况需要讨论
-
\(i>R\) 时,令 \(p_i=1\) ,并暴力向两侧扩展
因为此时我们统计出来的这个区间与该位置没有关系,只能暴力计算
-
当 \(i\leq R\) 时,首先需要找到点 \(i\) 关于 \(mid\) 的对称点 \(i^{'}\) \((i^{'}=mid\times 2 - i)\),然后进一步分类讨论
-
当 \(p_{i^{'}}\) 所对应的回文区间不包含 \(L\) 时
\[黄色直线代表以\;i\;为中心的回文区间,对于 i^{'}同理 \]由于以 \(i^{'}\) 为中心的回文串与以 \(i\) 为中心的回文串也关于 \(mid\) 对称,因此不难得出 \(p_i=p_{i^{'}}\)
-
当 \(p_{i^{'}}\) 所对应的回文区间包含 \(L\) 时
\[黄色直线代表以\;i\;为中心的回文区间在[L,R]内的部分,红色直线代表\;i^{'}\;的回文区间超出[L,R]的部分 \]可以通过上一条性质,发现黄色部分是可以继承的,但是红色部分是不确定的,因此需要再暴力匹配。即 \(p_i=R-i+1\) 后暴力扩展
-
至此全部情况已经讨论结束,但是当前情况未免过于臃肿,因此我们可以对这些情况进行化简。
-
当 \(p_i\) 对应的回文区间不包含 \(L\) 时,则 \(L<i^{'}-p_{i^{'}}+1\) ,又有 \(L=2\times mid -R\) 和 \(i^{'} =mid\times 2 - i\) 则 \((2\times mid - R) < (2 \times mid - i) - p_{2 \times mid - i} +1\)
化简式子可得 \(p_{2 \times mid - i} < R - i + 1\) ,即当 \(p_{2 \times mid -i}<R-i+1\) 时, 令 \(p_i=p_{2\times mid-i}\)
-
当 \(p_i\) 对应的回文区间包含 \(L\) 时,则 \(i^{'}-p_{i^{'}}+1\leq L\) ,又有 \(L=2\times mid-R\) 和 \(i^{'} =mid\times 2 - i\) ,则 \(2\times mid -i-p_{2\times mid-i}+1\leq 2\times mid-R\)
化简式子可得 \(R-i+1\leq p_{2\times mid -i}\) ,即当 \(R-i+1\leq p_{2\times mid -i}\) 时,令 \(p_i=R-i+1\) 并继续暴力扩展
通过上述两种情况的归纳可得,当 \(i\leq R\) 时,令 \(p_i=\min{(p_{2\times mid-i},R-i+1)}\) 并继续暴力扩展
代码实现
点击查看代码
struct manacher
{
char s[N << 2];
int n;
int p[N << 2];
void init(string &str)
{
n = str.size();
s[0] = s[1] = '|';
for (int i = 0; i < n; i++)
{
s[i * 2 + 2] = str[i];
s[i * 2 + 3] = '|';
}
n += n + 2;
s[n] = 0;
}
int work()
{
int mid = 1, mx = 1, res = -1;
for (int i = 1; i < n; i++)
{
if (i < mx)
p[i] = min(mx - i, p[mid * 2 - i]);
else
p[i] = 1;
while (s[i - p[i]] == s[i + p[i]])
p[i]++;
if (mx < i + p[i])
{
mid = i;
mx = i + p[i];
}
res = max(res, p[i] - 1);
}
return res;
}
};
例题
没什么水平,裸板子题。只需要跑一遍板子即可
一眼顶针!只需要找到最小的 \(i\) 满足其回文半径可以扩展到串尾即可