Manacher
描述:
给定一个长度为 n n n 的字符串 s s s ,请找到所有对 ( i , j ) (i,j) (i,j)使得子串 s [ i . . . j ] s[i...j] s[i...j]为一个回文串。当 t = t r e v 时 , 字 符 串 t 是 一 个 回 文 串 ( t r e v 是 t 的 反 转 字 符 串 ) t = t_{rev}时,字符串t是一个回文串(t_rev是t的反转字符串) t=trev时,字符串t是一个回文串(trev是t的反转字符串)
更进一步的描述:
显然在最坏情况下可能有 O ( n 2 ) O(n^2) O(n2)个回文串,因此似乎一眼看过去该问题并没有线性算法。
但是关于回文串的信息可用 一种更紧凑的方式 表达:对于每个位置 i = 0... n − 1 , 我 们 找 出 值 d 1 [ i ] 和 d 2 [ i ] i = 0...n-1,我们找出值 d_1[i]和 d_2[i] i=0...n−1,我们找出值d1[i]和d2[i]。二者分别表示以位置 i i i为中心的长度为奇数和长度为偶数的回文串个数。换个角度,二者也表示了以位置 i i i为中心的最长回文串的半径长度(半径长度 d 1 [ i ] , d 2 [ i ] d_1[i],d_2[i] d1[i],d2[i]均为从位置 i i i到回文串最右端位置包含的字符个数)。
-
举例来说,字符串 s = a b a b a b c s = abababc s=abababc以 s [ 3 ] = b s[3] = b s[3]=b为中心有三个奇数长度的回文串,最长回文串半径为3,也即 d 1 [ 3 ] = 3 d_1[3] = 3 d1[3]=3:
-
eg2: 字符串 s = c b a a b d s = cbaabd s=cbaabd以 s [ 3 ] = a s[3] = a s[3]=a为中心有两个偶数长度的回文串,最长回文串半径为2,也即 d 2 [ 3 ] = 2 d_2[3] = 2 d2[3]=2:
因此关键思路是,如果以某个位置 i i i 为中心,我们有一个长度为 l l l的回文串,那么我们有以 i i i 为中心的长度为 l − 2 l-2 l−2, l − 4 l-4 l−4,等等的回文串。所以 d 1 [ i ] 和 d 2 [ i ] d_1[i]和d_2[i] d1[i]和d2[i]两个数组已经足够表示字符串中所有回文串的信息。
一个更令人惊讶的事实,存在一个复杂度为线性并且足够简单的计算上述两个“回文性质数组” d 1 [ ] 和 d 2 [ ] 。 d_1[]和d_2[]。 d1[]和d2[]。
解法:
总的来说,该问题有很多种解法:应用字符哈希,该问题可在 O ( n l o g n ) O(nlogn) O(nlogn)时间内解决,而使用后缀数组和快速 L C A LCA LCA该问题可在 O ( n ) O(n) O(n)时间内解决。
但是这里描述的算法 压倒性 的简单,并且在时间和空间复杂度上具有更小的常数。该算法由 Glenn K. Manacher 在 1975 年提出。
朴素算法:
为了避免在之后的叙述中出现歧义,这里我们指出什么是"朴素算法"。
该算法通过下述方式工作:对每个中心位置 i i i,在比较一对对应字符后,只要可能,该算法便将答案加1。
时间复杂度 O ( n 2 ) O(n^2) O(n2)的时间内计算答案。
该朴素算法的实现如下:
//vector<int> d1(n),d2(n);
//char s[N]; int d1[N], d2[N];
for(int i = 0; i < n; i ++ ){
d1[i] = 1;
while(0 <= i - d1[i] && i + d1[i] < n && s[i-d1[i]] == s[i + d1[i]]) d1[i]++;
d2[i] = 0;
while(0 <= i - d2[i]-1 && i+d2[i] < n && s[i-d2[i]-1] == s[i+d2[i]])d2[i]++;
}
Manacher算法
这里我们将只描述算法中寻找所有奇数长度回文串的情况,即只计算 d 1 [ ] d_1[] d1[];寻找所有偶数长度回文串的算法(即计算数组 d 2 [ ] d_2[] d2[])将只需要对奇数情况下的算法进行一些小修改。
为了快速计算,我们维护已经找到的最靠右边的子回文串的边界 ( l , r ) (l,r) (l,r)(即具有最大 r 值的回文串, 其中 l 和 r 分别为该回文串左右边界的位置)。 初始时,我们置 l = 0 和 r = − 1 l = 0 和 r = -1 l=0和r=−1(-1需区别于倒叙索引位置,这里可为任意负数,仅为了循环初始时方便)。
现在假设我们要对下一个 i i i 计算 d 1 [ i ] d_1[i] d1[i],而之前所有 d 1 [ ] d_1[] d1[]中的值已经计算完毕。我们将通过下列方式计算:
- 如果 i i i位于当前子回文串之外,即 i > r i > r i>r, 那么我们调用朴素算法。
因此我们将连续地增加 d 1 [ i ] d_1[i] d1[i],同时在每一步中检查当前的子串 [ i − d 1 [ i ] . . . i + d 1 [ i ] ] [i - d_1[i]...i+d_1[i]] [i−d1[i]...i+d1[i]]( d 1 [ i ] d_1[i] d1[i]表示半径长度, 下同) 是否为一个回文串。如果我们找到了第一处对应字符不同,又或者碰到了 s s s 的边界,则算法停止。在两种情况下我们均已计算完 d 1 [ i ] d_1[i] d1[i]。此后,仍需记得更新 ( l , r ) (l,r) (l,r)。
- 现在考虑 i <= r 的情况。 我们将尝试从计算过的 d 1 [ ] 的 值 中 d_1[]的值中 d1[]的值中获取一些信息。首先在子回文串 ( l , r ) (l,r) (l,r)中反转位置 i i i, 即我们得到 j = l + ( r − i ) j = l + (r-i) j=l+(r−i)。现在我们来考虑 d 1 [ j ] d_1[j] d1[j]。因为位置 j j j同位置 i i i对称(在之前维护的最大回文串里),我们几乎总是可以置 d 1 [ i ] = d 1 [ j ] d_1[i] = d_1[j] d1[i]=d1[j]。
- 有一个棘手的情况需要被正确处理:当“内部”的回文串到达“外部”回文串的边界时,即 j − d 1 [ j ] + 1 ≤ l j - d_1[j] + 1 \le l j−d1[j]+1≤l(或者等价的说$ i + d_1[j]-1 \ge r$)。因为在”外部“回文串范围以外的对称性没有保证,因此直接置 d 1 [ i ] = d 1 [ j ] d_1[i] = d_1[j] d1[i]=d1[j]将是不正确的;我们没有足够的信息来断言在位置 i i i 的回文串具有同样的长度。
- 实际上,为了正确处理这种情况,我们应该“截断”回文串的长度,即置 d 1 [ i ] = r − i d_1[i] = r-i d1[i]=r−i。之后我们将运行朴素算法以尝试尽可能增加 d 1 [ i ] 的 值 d_1[i]的值 d1[i]的值。
- 尽管以 j j j 为中心的回文串可能更长,以致于超出“外部”回文串,但在位置 i i i,我们只能利用其完全落在“外部”回文串内的部分。然而位置 i i i 的答案可能比这个值更大, 因此接下来我们将运行朴素算法来尝试将其扩展至“外部”回文串之外。
- 最后
记得更新
d 1 [ i ] d_1[i] d1[i]后更新值 ( l , r ) (l,r) (l,r)
Manacher 算法的复杂度¶
因为在计算一个特定位置的答案时我们总会运行朴素算法,所以一眼看去该算法的时间复杂度为线性的事实并不显然。
然而更仔细的分析显示出该算法具有线性复杂度。此处我们需要指出,计算 Z 函数的算法 和该算法较为类似,并同样具有线性时间复杂度。
实际上,注意到朴素算法的每次迭代均会使 r r r 增加 1,以及 r r r 在算法运行过程中从不减小。这两个观察告诉我们朴素算法总共会进行 O ( n ) O(n) O(n) 次迭代。
Manacher 算法的另一部分显然也是线性的,因此总复杂度为 O ( n ) O(n) O(n)。
代码实现:
分类讨论
//vector<int> d1(n);
//char s[N], d1[N];
//求奇数长度回文串的情况
int l = 0, r = -1;
for(int i = 0; i < n; i ++ ){
int k = (i > r) ? 1 : min(d2[l + r - i], r - i);
while(0 <= i - k && i + k < n && s[i - k] == s[i + k]) k ++;
d1[i] = k--;
if(i + k > r){
l = i- k;
r = i + k;
}
}
//类比z函数的写法(这个还没debug。。。。)
//(l,r)
int l = 0, r = 0;
for(int i = 0; i < n; i ++ ) z[i] = 1;
for (int i = 1; i < n; i++) {
if (i < r)
z[i] = min(r - i, z[l + r - i]);
while(i >= z[i] && i + z[i] < n && s[i - z[i]] == s[i + z[i]]) z[i]++;
if (i + z[i] > r) {
l = i - z[i];
r = i + z[i];
}
}
//vector<int> d2(n)
//char s[N], d2[N];
//求偶数长度的回文串
//1 2 s[i] 1
int l = 0, r = -1;
for(int i = 0; i < n; i ++ ){
int k = (i > r) ? 0 : min(d2[l + r - i + 1], r - i + 1);
while( 0 <= i - k - 1 && i + k < n && s[i - k - 1] == s[i + k]) k ++;
d2[i] = k--;
if(i + k > r){
l = i - k -1;
r = i + k;
}
}
//类比z函数的写法[l,r)
//1 2 s[i] 1
int l = 0, r = 0;
for (int i = 1; i < n; i++) {
if (i < r)
z[i] = min(r - i, z[l + r - i]);
while(i > z[i] && i + z[i] < n s[i - z[i] - 1] == s[i + z[i]]) z[i]++;
if (i + z[i] > r) {
l = i - z[i];
r = i + z[i];
}
}
统一处理
虽然分开计算 d 1 [ ] , d 2 [ ] d_1[],d_2[] d1[],d2[],但是可以通过一个技巧将二者的计算统一为 d 1 [ ] d_1[] d1[]的计算。
给定一个长度为 n n n 的字符串 s s s, 我们在其 n + 1 n + 1 n+1个空中插入分割符 # \# #,从而构造一个长度为 2 n + 1 2n + 1 2n+1的字符串 s ′ s' s′.举例来说,对于字符串 s = a b a b a b c s = abababc s=abababc,其对应的 s ′ = # a # b # a # b # a # b # c # s' = \#a\#b\#a\#b\#a\#b\#c\# s′=#a#b#a#b#a#b#c#。
对于字母间的 # \# # ,其实际意义为 s s s 中对应的“空”。而两端的 # \# #则是为了实现的方便。
注意到,在对 s ′ s' s′ 计算 d 1 [ ] d_1[] d1[] 后,对于一个位置 i i i, d 1 [ i ] d_1[i] d1[i]所描述的最长的子回文串必定以 # \# # 结尾(若以字母结尾,由于字母两侧必定各有一个 # \# #,因此可向外扩展一个得到一个更长的)。因此,对于 s s s 中一个以字母为中心的极大子回文串,设其长度为 m + 1 m+1 m+1,则其在 s ′ s' s′ 中对应一个以相应字母为中心,长度为 2 m + 3 2m+3 2m+3 的极大子回文串;而对于 s s s 中一个以空为中心的极大子回文串,设其长度为 m m m,则其在 s ′ s' s′中对应一个以相应表示空的 # \# #为中心,长度为 2 m + 1 2m+1 2m+1 的极大子回文串(上述两种情况下的 m m m均为偶数,但该性质成立与否并不影响结论)。综合以上观察及少许计算后易得,在 s ′ s' s′ 中, d 1 [ i ] d_1[i] d1[i] 表示在 s s s中以对应位置为中心的极大子回文串的 总长度加一。
上述结论建立了 s ′ s' s′ 的 d 1 [ ] d_1[] d1[] 同 s s s 的 d 1 [ ] d_1[] d1[] 和 d 2 [ ] d_2[] d2[] 间的关系。
由于该统一处理本质上即求 s ′ s' s′ 的 d 1 [ ] d_1[] d1[] ,因此在得到 s ’ s’ s’ 后,代码同上节计算 d 1 [ ] d_1[] d1[] 的一样。
板子1:(统一处理)
const int N = 2e7 + 6;//记得开两倍!!!!
int n;
char s[N], str[N];
void init(){
int k = 0;
s[k++] = '$';
s[k++] = '#';
for(int i = 0; i < n; i ++ )s[k++] = str[i], s[k++] = '#';
s[k++] = '^';
n = k;
}
int z[N];
void manacher(){
[l,r)
int mid = 0, rr = 0;
for(int i = 1; i < n; i ++ ){
if(i < rr)z[i] = min(z[mid*2-i], rr-i);
else z[i] = 1;
while(s[i-z[i]] == s[i+z[i]]) z[i]++;
if(i + z[i] > rr){
rr = i + z[i];
mid = i;
}
}
}
/*说明:(i为原串中的位置)
对于奇数长度的串 aaaiaaa
int len1 = z[i*2+2]-1
int r1 = len1+1>>1;
对于偶数长度的串 aaiaaa
int len = z[i*2+3]-1
int r2 = len2>>1;
*/
int main() {
//ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
cin>>str;
n = strlen(str);
init();
manacher();
int res = 0;
for(int i = 0; i < n; i ++ ) res = max(res,z[i]);
cout<<res-1<<endl;//减1,是因为肯定会扩充到#
return 0;
}
板子2:
cf961 f k-substrings
给定一个字符串 S
求所有的
S
[
i
,
n
−
i
+
1
]
的
b
o
r
d
e
r
S[i,n−i+1]的 border
S[i,n−i+1]的border 长度(最长的前缀等于后缀),要求长度是奇数
n
≤
1
0
6
n≤10^6
n≤106
思路:
- 首加尾,构造回文:
scanf("%s", s);
for (int i = n - 1; i > 0; i--)
s[2 * i] = s[i];
for (int i = 1; i < 2 * n; i += 2)
s[i] = s[2 * n - 1 - i];
那么也就是求这个串的以某个位置的开始的最长回文串
- 由于题目是求奇数长度的,那么对于 s ′ s' s′求一遍马拉车,只用到前一半(n)就行了。
- 对于 s ′ s' s′中的一个回文串 t t tt tt,每次长度减少2.在原串中,其实是删除一个元素。
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <algorithm>
#include <cmath>
#include <vector>
#include <set>
#include <map>
#include <unordered_set>
#include <unordered_map>
#include <queue>
#include <ctime>
#include <cassert>
#include <complex>
#include <string>
#include <cstring>
using namespace std;
typedef long long ll;
typedef pair<int, int> pii;
#define mp make_pair
const int N = (int)2e6 + 100;
int n;
char s[N];
int z[N];
pii st[N];
int m;
int ans[N];
int main()
{
// freopen("input.txt", "r", stdin);
// freopen("output.txt", "w", stdout);
scanf("%d", &n);
scanf("%s", s);
for (int i = n - 1; i > 0; i--)
s[2 * i] = s[i];
for (int i = 1; i < 2 * n; i += 2)
s[i] = s[2 * n - 1 - i];
int l = 0, r = 0;
//马拉车
for (int i = 1; i < n; i++) {
if (i < r)
z[i] = min(r - i, z[l + r - i]);
while(i > z[i] && s[i - z[i] - 1] == s[i + z[i]]) z[i]++;
if (i + z[i] > r) {
l = i - z[i];
r = i + z[i];
}
}
for (int i = 1; i < n; i += 2) {
l = i - z[i];
//用一个单调栈,来维护左端点,最靠左的点。i越大,的回文长度越长。
while(m > 0 && st[m - 1].first >= l) m--;
st[m++] = mp(l, i);
}
for (int i = 0; i <= n; i++)
ans[i] = -1;
for (int i = 0; i < m; i++) {
r = st[i].second;
if (i + 1 < m) r = min(r, st[i + 1].first);
for (int j = st[i].first; j < r; j++)
ans[j] = st[i].second - j;
}
for (int i = 0; i < n; i += 2)
printf("%d ", ans[i]);
printf("\n");
return 0;
}