某 SCOI 模拟赛 T3 c【回文自动机 树链剖分 倍增】

题意

有一个字符串 s s s Σ = 26 \Sigma=26 Σ=26),记 f ( t ) f(t) f(t) t t t s s s 中出现的次数,有 m m m 次以下操作:

  • addl ⁡   c \operatorname{addl}\ c addl c:在 s s s 左侧添加字符 c c c
  • addr ⁡   c \operatorname{addr}\ c addr c:在 s s s 右侧添加字符 c c c
  • transl ⁡   l 1   r 1   l 2   r 2 \operatorname{transl}\ l_1\ r_1\ l_2\ r_2 transl l1 r1 l2 r2:询问:假如通过在 s [ l 1 … r 1 ] s[l_1\dots r_1] s[l1r1] 左侧添加或删除字符,在最小步数内使其变成 s [ l 2 … r 2 ] s[l_2\dots r_2] s[l2r2],在这过程中出现的所有回文串 t t t ∣ t ∣ f ( t ) |t|f(t) tf(t) 之和;
  • transr ⁡   l 1   r 1   l 2   r 2 \operatorname{transr}\ l_1\ r_1\ l_2\ r_2 transr l1 r1 l2 r2:询问:假如通过在 s [ l 1 … r 1 ] s[l_1\dots r_1] s[l1r1] 右侧添加或删除字符,在最小步数内使其变成 s [ l 2 … r 2 ] s[l_2\dots r_2] s[l2r2],在这过程中出现的所有回文串 t t t ∣ t ∣ f ( t ) |t|f(t) tf(t) 之和。

例:把 abb transl ⁡ \operatorname{transl} transldbbcbb,中途出现的字符串有:abb -> bb -> cbb -> bcbb -> bbcbb -> dbbcbb,其中 bbbbcbb 是回文的,答案为 ∣ bb ∣ f ( bb ) + ∣ bbcbb ∣ f ( bbcbb ) |\texttt{bb}|f(\texttt{bb})+|\texttt{bbcbb}|f(\texttt{bbcbb}) bbf(bb)+bbcbbf(bbcbb)

题解

首先明确一点:正、反串的 PAM 形态是一样的:

考虑到回文串正着看、反着看都一样,实际上回文串的最长回文前缀,也就是其最长回文后缀。所以 f a i l ′ = f a i l fail'=fail fail=fail,只维护 f a i l fail fail 即可。
从自己博客抄的

我们先只考虑 transl ⁡ \operatorname{transl} transl,另一个方向同理。显然在 transl ⁡ \operatorname{transl} transl 中出现过的字符串为 s [ l 1 … r 1 ] s[l_1\dots r_1] s[l1r1] 的所有长度在 r 1 − l 1 + 1 r_1-l_1+1 r1l1+1 ∣ lcs ⁡ (最长公共后缀) ( s [ l 1 … r 1 ] ) , s [ l 2 … r 2 ] ) ∣ |\operatorname{lcs}\text{\footnotesize(最长公共后缀)}(s[l_1\dots r_1]),s[l_2\dots r_2])| lcs(最长公共后缀)(s[l1r1]),s[l2r2]) 之间的后缀、以及 s [ l 2 … r 2 ] s[l_2\dots r_2] s[l2r2] 的所有长度在 r 2 − l 2 + 1 r_2-l_2+1 r2l2+1 ∣ lcs ⁡ ( s [ l 1 … r 1 ] ) , s [ l 2 … r 2 ] ) ∣ |\operatorname{lcs}(s[l_1\dots r_1]),s[l_2\dots r_2])| lcs(s[l1r1]),s[l2r2]) 之间的后缀。假如不考虑回文,那这就是在 SAM 的 fail 树上的一条路径;考虑回文,那这就是在 PAM 的 fail 树上的一条路径,我们要统计其 l e n × f len\times f len×f 之和。

找到这条路径,首先要找到路径的两个端点。我们先把询问离线下来,把询问的所有位置改为以最终串为标准的位置,建出最终串的 PAM,再倒着“建”一遍,处理出以 p o s pos pos 为开头以及结尾的最长回文串。 transl ⁡ \operatorname{transl} transl 时,找到 r 1 r_1 r1 结尾的最长回文子串,倍增沿 fail 往上跳(也可以树剖维护)到第一个长度不大于 r 1 − l 1 + 1 r_1-l_1+1 r1l1+1 的回文串,作为一个端点( r 2 r_2 r2 同理)。注意 LCA 可能不统计入内,此时 LCA 对应的回文串不是两询问串的 lcs,判断时只需要看 s [ r 1 − L C A . l e n ] s[r_1-LCA.len] s[r1LCA.len] s [ r 2 − L C A . l e n ] s[r_2-LCA.len] s[r2LCA.len] 是否相等即可。

考虑如何维护 l e n × f len\times f len×f。首先将最终串的 fail 树建出来,新加入一个字符(假设在左边)时,找到以该字符为开头的最长回文串,倍增沿 fail 往上跳(也可以树剖维护)到首个长度不超过当前串长的节点,则这个节点到根路径上所有节点的出现次数都会加一。于是我们要维护的有:到根路径上节点的 f f f 加一、询问一条路径的 f × l e n f\times len f×len 之和。可以用树剖或 LCT 维护。

综上:离线 + PAM + 倒着跑一遍 PAM + 树剖 + 倍增 预处理;修改操作先倍增向上跳,再修改到根路径;查询操作先倍增向上跳,再查询两点间路径,并特判 LCA。

代码有点长,不过不算很难写:

#include<bits/stdc++.h>
using namespace std;
#define ll long long
int getint(){
	int ans=0,f=1;
	char c=getchar();
	while(c<'0'||c>'9'){
		if(c=='-')f=-1;
		c=getchar();
	}
	while(c>='0'&&c<='9'){
		ans=ans*10+c-'0';
		c=getchar();
	}
	return ans*f;
}
const int N=3e5+10,L=19,S=30;
int s[N],l,r;
int n;

/***** PAM *****/ 
int len[N],ch[N][S],fail[N],cnt;
int lst,tsl;
int pre[N],suf[N];
void init_pam(){ len[1]=-1;fail[1]=fail[0]=1;cnt=2; }
void extend(int c,int pos){
	int cur=lst;
	while(s[pos-len[cur]-1]!=c)cur=fail[cur];
	if(!ch[cur][c]){
		int t=cnt++;
		len[t]=len[cur]+2;
		fail[t]=fail[cur];
		while(s[pos-len[fail[t]]-1]!=c)fail[t]=fail[fail[t]];
		fail[t]=ch[fail[t]][c];
		ch[cur][c]=t;
	}
	lst=ch[cur][c];
	pre[pos]=lst;
	//if(len[lst]==size)tsl=lst;
}
void dnetxe(int c,int pos){
	int cur=tsl;
	while(s[pos+len[cur]+1]!=c)cur=fail[cur];
	if(!ch[cur][c]){
		int t=cnt++;
		len[t]=len[cur]+2;
		fail[t]=fail[cur];
		while(s[pos+len[fail[t]]+1]!=c)fail[t]=fail[fail[t]];
		fail[t]=ch[fail[t]][c];
		ch[cur][c]=t;
	}
	tsl=ch[cur][c];
	suf[pos]=tsl;
	//if(len[tsl]==size)lst=tsl;
}

/***** Tree *****/
struct bian{
	int e,n;
};
bian b[N<<1];
int st[N],tot=0;
void add(int x,int y){
	tot++;
	b[tot].e=y;
	b[tot].n=st[x];
	st[x]=tot;
}
int dep[N],f[L][N],sz[N],mxson[N];
int dfn[N],dfnend[N],nfd[N],top[N],dfnn=0;
void ss(int x){
	sz[x]=1;
	mxson[x]=cnt+10;
	for(int i=st[x];i;i=b[i].n){
		f[0][b[i].e]=x;
		dep[b[i].e]=dep[x]+1;
		ss(b[i].e);
		sz[x]+=sz[b[i].e];
		if(sz[b[i].e]>sz[mxson[x]])mxson[x]=b[i].e;
	}
}
void ss2(int x,int t){
	dfn[x]=dfnend[x]=++dfnn;
	nfd[dfn[x]]=x;
	top[x]=t;
	if(mxson[x]<cnt)ss2(mxson[x],t);
	for(int i=st[x];i;i=b[i].n){
		if(b[i].e==mxson[x])continue;
		ss2(b[i].e,b[i].e);
	}
}
void init_lca(){
	for(int i=1;i<L;i++){
		for(int j=0;j<cnt;j++){
			f[i][j]=f[i-1][f[i-1][j]];
		}
	}
}
int lca(int x,int y){
	if(dep[x]<dep[y])swap(x,y);
	for(int i=L-1;i>=0;--i){
		if(dep[f[i][x]]>=dep[y])x=f[i][x];
	}
	if(x==y)return x;
	for(int i=L-1;i>=0;--i){
		if(f[i][x]!=f[i][y])x=f[i][x],y=f[i][y];
	}
	return f[0][x];
}

/***** Segment tree *****/
ll a[N<<2],tag[N<<2];
ll sum[N]; 
void init_sum(){
	for(int i=1;i<=dfnn;i++)sum[i]=sum[i-1]+len[nfd[i]];
}
void pushdown(int x,int l,int r){
	int mid=l+r>>1;
	tag[x<<1]+=tag[x];
	tag[x<<1|1]+=tag[x];
	a[x<<1]+=(sum[mid]-sum[l-1])*tag[x];
	a[x<<1|1]+=(sum[r]-sum[mid])*tag[x];
	tag[x]=0;
}
void pushup(int x){
	a[x]=a[x<<1]+a[x<<1|1];
}
void modify(int l,int r,int val,int x,int nl,int nr){
	if(nr<l||nl>r)return;
	if(l<=nl&&nr<=r){
		a[x]+=(sum[nr]-sum[nl-1])*val;
		tag[x]+=val;
		return;
	}
	pushdown(x,nl,nr);
	int mid=nl+nr>>1;
	modify(l,r,val,x<<1,nl,mid);
	modify(l,r,val,x<<1|1,mid+1,nr);
	pushup(x);
}
ll query(int l,int r,int x,int nl,int nr){
	if(nr<l||nl>r)return 0;
	if(l<=nl&&nr<=r) return a[x];
	pushdown(x,nl,nr);
	int mid=nl+nr>>1;
	ll ans=query(l,r,x<<1,nl,mid)+query(l,r,x<<1|1,mid+1,nr);
	return ans;
}
void modify(int x,int val){
	while(x<cnt){
		int t=top[x];
		modify(dfn[t],dfn[x],val,1,1,dfnn);
		x=f[0][t];
	}
}
ll query(int x){
	ll ans=0;
	while(x<cnt){
		int t=top[x];
		ans+=query(dfn[t],dfn[x],1,1,dfnn);
		x=f[0][t];
	}
	return ans;
}

/***** Operations *****/
void add(char di,int pos,int len){
	pos=(di=='l'?suf[pos]:pre[pos]);
	for(int i=L-1;i>=0;--i)
		if(::len[f[i][pos]]>len)pos=f[i][pos];
	if(::len[pos]>len)pos=f[0][pos];
	modify(pos,1);
}
ll transl(int r1,int r2,int len1,int len2){
	int r11=r1,r22=r2;
	r1=pre[r1],r2=pre[r2];
	for(int i=L-1;i>=0;--i){
		if(len[f[i][r1]]>len1)r1=f[i][r1];
		if(len[f[i][r2]]>len2)r2=f[i][r2];
	}
	if(len[r1]>len1)r1=f[0][r1];
	if(len[r2]>len2)r2=f[0][r2];
	int l=lca(r1,r2);
	if(len[l]!=len1&&len[l]!=len2&&s[r11-len[l]]==s[r22-len[l]]){
		return query(r1)+query(r2)-query(l)*2;
	}else{
		return query(r1)+query(r2)-query(l)-query(f[0][l]);
	}
}
ll transr(int l1,int l2,int len1,int len2){
	int l11=l1,l22=l2;
	l1=suf[l1],l2=suf[l2];
	for(int i=L-1;i>=0;--i){
		if(len[f[i][l1]]>len1)l1=f[i][l1];
		if(len[f[i][l2]]>len2)l2=f[i][l2];
	}
	if(len[l1]>len1)l1=f[0][l1];
	if(len[l2]>len2)l2=f[0][l2];
	int l=lca(l1,l2);
	if(len[l]!=len1&&len[l]!=len2&&s[l11+len[l]]==s[l22+len[l]]){
		return query(l1)+query(l2)-query(l)*2;
	}else{
		return query(l1)+query(l2)-query(l)-query(f[0][l]);
	}
}

struct que{
	int op;
	int c;
	int l1,r1,l2,r2;
};
que q[N];
char tmp[10];

int main(){
	n=getint();int m=getint();
	l=100001;r=l+n;
	for(int i=l;i<r;i++){
		s[i]=getint()+1;
	}
	for(int i=0;i<m;i++){
		scanf("%s",tmp);
		if(tmp[0]=='a'){
			if(tmp[3]=='l')q[i].op=0,s[--l]=q[i].c=getint()+1;
			else q[i].op=1,s[r++]=q[i].c=getint()+1;
		}
		if(tmp[0]=='t'){
			if(tmp[5]=='l')q[i].op=2;
			else q[i].op=3;
			q[i].l1=l+getint()-1,q[i].r1=l+getint()-1;
			q[i].l2=l+getint()-1,q[i].r2=l+getint()-1;
		}
	}
	init_pam(); 
	for(int i=l;i<r;i++)extend(s[i],i);
	for(int i=r-1;i>=l;--i)dnetxe(s[i],i);
	
	for(int i=0;i<cnt;i++){
		if(fail[i]!=i)add(fail[i],i);
	}
	for(int i=0;i<L;i++)for(int j=0;j<N;j++)f[i][j]=cnt+10;
	dep[1]=1;ss(1);ss2(1,1);
	init_lca();
	init_sum();
	
	int le=100001,ri=le+n; 
	for(int i=le;i<ri;i++)add('r',i,i-le+1);
	for(int i=0;i<m;i++){
		if(q[i].op==0){
			--le;add('l',le,ri-le);
		}
		if(q[i].op==1){
			add('r',ri,ri-le+1);++ri;
		} 
		if(q[i].op==2){
			printf("%lld\n",
				transl(q[i].r1,q[i].r2,q[i].r1-q[i].l1+1,q[i].r2-q[i].l2+1));
		}
		if(q[i].op==3){
			printf("%lld\n",
				transr(q[i].l1,q[i].l2,q[i].r1-q[i].l1+1,q[i].r2-q[i].l2+1));
		}
	}
	return 0;
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值