mancher

11 篇文章 1 订阅
10 篇文章 0 订阅

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=trevttrevt

更进一步的描述:

显然在最坏情况下可能有 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...n1,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 l2, l − 4 l-4 l4,等等的回文串。所以 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=0r=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]] [id1[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+(ri)。现在我们来考虑 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 jd1[j]+1l(或者等价的说$ 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]=ri。之后我们将运行朴素算法以尝试尽可能增加 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,ni+1]border 长度(最长的前缀等于后缀),要求长度是奇数
n ≤ 1 0 6 n≤10^6 n106

思路:

  • 首加尾,构造回文:
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;
}

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值