马拉车--相交回文串,最长双回文串问题

Manacher 算法流程

  1. 为了不判断奇偶,方便比较,在原字符串里添加一些特殊字符

  2. 明确几个变量,\(r\)代表我遍历过的位置中回文串能延伸的最远位置,\(mid\)\(r\)对应的中心位置,那么\(mid\)对应的左端点就是\(2mid-r\)\(f_i\)表示\(i\)为中心的回文半径长度

  3. 接下来就分以下情况讨论,可以自己画图理解(我并不会画图)

    1. 我当前位置\(i<r\),我一定能在包含他的最长回文串的对称部分找到一个在最长回文串包含范围内和他相同的回文串(因为字母本身也是一个回文串,这样我就统一说成回文串了),但是我们并不能保证在最长回文串外部他们俩还相等,所以我要在\(f_{2\times mid-i}\)\(r-i+1\)之间取较小的一个

    2. 承接我上面的\(1\),因为我只在最长回文串内部判断了它的回文串,回文串外部的还没有判断,所以我向两边扩展

    3. 在我判断的过程中不断更新\(mid\)\(r\)

code:

#include <iostream>
#include <algorithm>
#include <cstdio>
using namespace std;
int read(){
	int x = 1,a = 0;char ch = getchar();
	while (ch < '0'||ch > '9'){if (ch == '-') x = -1;ch = getchar();}
	while (ch >= '0'&&ch <= '9'){a = a*10+ch-'0';ch = getchar();}
	return x*a;
}
const int maxn = 5e7+10;
char  a[maxn];
int cnt;
void init(){
	char ch = getchar();cnt = 1;
    a[0]='~',a[cnt]='|';
    while(ch < 'a'||ch > 'z') ch = getchar();
    while(ch >= 'a'&&ch <= 'z') a[++cnt] = ch,a[++cnt] = '|',ch = getchar();
}
int f[maxn],ans;
int main(){
	init();
	for(int i = 1,r = 0,mid = 0;i <= cnt;i++){
	    if(i <= r) f[i] = min(f[(mid<<1)-i],r-i+1);
	    while(a[i-f[i]] == a[i+f[i]]) ++f[i];
	    if(f[i]+i > r) r = f[i]+i-1,mid = i;
	    if(f[i] > ans) ans = f[i];
      }
      printf("%d\n",ans-1);
	return 0;
}

例题:

相交的回文串对数,我们可以转化为总回文串对数-不相交的回文串对数,两个回文串不相交,那么他们中间一定有至少一个断点,枚举断点,恰好使他是被分割开的后面的回文串的开头(这样才可以保证不被重复计算),再看在他左边有多少个回文串的结尾乘法原理做就好了。

当我们有一个半径为\(r\)的回文串那么我能得到的回文串个数也是\(r\),马拉车可以处理出以每个字符为中心的回文串半径,但是注意我们添加了字符,所以我的半径其实就是直径+1,碰巧我的半径需要向上取证,所以我的回文串总数就是\(ans=\sum\limits_{i}^{cnt} \dfrac{f_i}{2}\),我总回文串的对数就是\(ans=\dfrac{ans \times (ans-1)}{2}\)

考虑我们如何求区间内回文串结尾的个数和一个点的回文串开头的个数,就像上面说的,当我有一个半径为\(r\)的回文串,其实它的左半径上的所有点都可以作为一个回文串的开头,右半径的所有点都可以作为一个回文串的结尾,其实这就是一个区间加的操作,二次差分就可以求出这个数量。

code:

#include <iostream>
#include <algorithm>
#include <cstdio>
#define int long long
using namespace std;
int read() {
	int x = 1,a = 0;char ch = getchar();
	while (ch < '0'||ch > '9') {if (ch == '-') x = -1;ch = getchar();}
	while (ch >= '0'&&ch <= '9') {a = a*10+ch-'0';ch = getchar();}
	return x*a;
}
const int maxn = 4e6+10,mod = 51123987;
char a[maxn];
int cnt,n;
void init() {
	scanf ("%lld",&n);
	char ch = getchar();
	cnt = 1;
	a[0]='~',a[cnt]='|';
	while(ch < 'a'||ch > 'z') ch = getchar();
	while(ch >= 'a'&&ch <= 'z') a[++cnt] = ch,a[++cnt] = '|',ch = getchar();
}
int f[maxn],ans;
int l[maxn],r1[maxn];
signed main() {
	init();
	for(int i = 1,r = 0,mid = 0; i <= cnt; i++) {
		if(i <= r) f[i] = min(f[(mid<<1)-i],r-i+1);
		while(a[i-f[i]] == a[i+f[i]]) ++f[i];
		if(f[i]+i > r) r = f[i]+i-1,mid = i;
	}
	for (int i = 1;i <= cnt;i++) l[i-f[i]+1]++,l[i+1]--,r1[i+f[i]]--,r1[i]++;
	for (int i = 1;i <= cnt;i++) (ans += f[i]/2)%=mod;
	ans = (ans*(ans-1)/2)%mod;
	for (int i = 1,s = 0;i <= cnt;i++){
		l[i] += l[i-1],r1[i] += r1[i-1];
		if (i%2 == 0) (ans -= s*l[i])%=mod,(s += r1[i])%=mod;
	}
	printf("%lld\n",(ans%mod+mod)%mod);
	return 0;
}

明确几个变量: \(r_i\)表示以\(i\)结尾的最长回文串的长度,\(l_i\)表示以\(i\)开头的最长回文串个数。

转移方程应该还是挺好想的,当我马拉车求出所有的半径之后:\(l_{i-f_i+1}=max(f_i+1,l_{i-f_i+1})\),\(r_{i+f_i-1}=max(f_i+1,r_{i+f_i-1})\)

但是直到上面我们只更新了两边的最长回文串长度,中间的最长回文串长度我怎么更新呢???

枚举断点'|',因为我们在原串中间添加了字符,所以每挪动一次断点回文串长度会-2,得到转移方程:

\(r_i=max(r_i,r_{i+2}-2)\)\(l_i=max(l_i,l_{i-2}-2)\)

最长双回文串长度就等于从同一点开始和结束的两个回文串长度相加,注意我这同一点一定是特殊字符断点

code:

#include <iostream>
#include <algorithm>
#include <cstdio>
#define int long long
using namespace std;
int read() {
	int x = 1,a = 0;char ch = getchar();
	while (ch < '0'||ch > '9') {if (ch == '-') x = -1;ch = getchar();}
	while (ch >= '0'&&ch <= '9') {a = a*10+ch-'0';ch = getchar();}
	return x*a;
}
const int maxn = 4e6+10,mod = 51123987;
char a[maxn];
int cnt,n;
void init() {
	char ch = getchar();
	cnt = 1;
	a[0]='~',a[cnt]='|';
	while(ch < 'a'||ch > 'z') ch = getchar();
	while(ch >= 'a'&&ch <= 'z') a[++cnt] = ch,a[++cnt] = '|',ch = getchar();
}
int f[maxn],ans;
int l[maxn],r1[maxn];
signed main() {
	init();
	for(int i = 1,r = 0,mid = 0; i <= cnt; i++) {
		if(i <= r) f[i] = min(f[(mid<<1)-i],r-i+1);
		while(a[i-f[i]] == a[i+f[i]]) ++f[i];
		if(f[i]+i > r) r = f[i]+i-1,mid = i;
	}
	for (int i = 1;i <= cnt;i++){
		r1[i+f[i]-1] = max(r1[i+f[i]-1],f[i]-1);
		l[i-f[i]+1] = max(l[i-f[i]+1],f[i]-1);
	}
	for (int i = 1;i <= cnt;i+=2){
		r1[i] = max(r1[i],r1[i+2]-2);
		l[i] = max(l[i],l[i-2]-2);
		if (r1[i]&&l[i]) ans= max(ans,l[i]+r1[i]);
	}
	printf("%d\n",ans);
	return 0;
}
posted @ 2020-11-12 08:39  小又又yyyy  阅读(113)  评论(0编辑  收藏  举报