【SAM+线段树合并】LGP4770 [NOI2018]你的名字

【题目】
原题地址
给定一个字符串 S S S,多组询问给定字符串 T T T以及两个数字 l , r l,r l,r。求 S [ l . . r ] S[l..r] S[l..r]中有多少个子串 x x x满足: x x x的任意一个子串没有在 T T T中出现过。 ∣ S ∣ , ∑ ∣ T ∣ ≤ 5 × 1 0 5 |S|,\sum |T| \leq 5\times 10^5 S,T5×105

【解题思路】
加深对 SAM \text{SAM} SAM的理解。

既然是字符串题,我们首先对 S S S T T T分别建 SAM \text{SAM} SAM
考虑 l = 1 , r = ∣ S ∣ l=1,r=|S| l=1,r=S的情况。
l i m i lim_i limi T [ 1 , i ] T[1,i] T[1,i]能匹配 S S S的最长后缀为 T [ i − l i m i + 1 , i ] T[i-lim_i+1,i] T[ilimi+1,i](若 l i m i = 0 lim_i=0 limi=0 T i T_i Ti没有在 S S S中出现过)。那么这个我们在 SAM \text{SAM} SAM上一路往下跑,匹配不了就往 f a fa fa跳,这样就可以简单处理出来,有点类似双指针。
SAM \text{SAM} SAM中节点 i i i r i g h t right right集合包含的字符串最大长度为 m x i mx_i mxi,字符串第一次出现的位置为 e n d i end_i endi(这个节点 r i g h t right right集中的每个串 e n d i end_i endi都是一样的,因为是后缀包含关系,终点等价)。我们枚举 T T T中的每个节点,考虑每条 i i i f a i fa_i fai边的贡献,那么:
a n s = ∑ i = 2 c n t m a x ( 0 , m x i − m a x ( m x f a i , l i m e n d i ) ) ans=\sum_{i=2}^{cnt} max(0,mx_i-max(mx_{fa_i},lim_{end_i})) ans=i=2cntmax(0,mximax(mxfai,limendi))
这个式子的意思就是对于每个节点,不属于 S S S的子串的总个数为当前节点所代表的集合字符串个数减去与 S S S有匹配的字符串个数。

于是现在实际上总的问题就是求这个 l i m i lim_i limi
我们可以对于 S S S SAM \text{SAM} SAM每个节点按 m x i mx_i mxi排序,每个节点建线段树表示出现位置,做线段树合并。那么每次我们求 l i m i lim_i limi,往下扩展的时候,我么只需要判断这个节点线段树中 [ l + l e n , r ] [l+len,r] [l+len,r]是否出现过即可。其中 l e n len len表示当前匹配了 T [ i − l e n + 1 , i ] T[i-len+1,i] T[ilen+1,i],出现了即表示字符串出现在 S [ l , r ] S[l,r] S[l,r]内。

复杂度 O ( P log ⁡ P ) O(P\log P) O(PlogP),我们取 P P P 1 0 6 10^6 106级别吧。

【参考代码】

#include
using namespace std;

typedef long long ll;
const int N=1e6+10,M=N*20;
int rt[N],b[N],c[N],lim[N];
char s[N];
ll ans;

int read()
{
	int ret=0;char c=getchar();
	while(!isdigit(c)) c=getchar();
	while(isdigit(c)) ret=ret*10+(c^48),c=getchar();
	return ret;
}

struct Segment
{
	int sz,ls[M],rs[M];
	void insert(int &x,int l,int r,int p)
	{
		if(!x) x=++sz;
		if(l==r) return;
		int mid=(l+r)>>1;
		if(p<=mid) insert(ls[x],l,mid,p);
		else insert(rs[x],mid+1,r,p);
	}
	int merge(int x,int y)
	{
		if(!x || !y) return x+y;
		int z=++sz;
		ls[z]=merge(ls[x],ls[y]);rs[z]=merge(rs[x],rs[y]);
		return z;
	}
	bool query(int x,int l,int r,int L,int R)
	{
		if(!x) return 0;
		if(L<=l && r<=R) return 1;
		int mid=(l+r)>>1;bool res=0;
		if(L<=mid) res|=query(ls[x],l,mid,L,R);
		if(R>mid) res|=query(rs[x],mid+1,r,L,R);
		return res;
	}
}tr;

struct SAM
{
	int sz,tot,las,n,mx[N],fa[N],pos[N],ch[N][26];
	void extend(int x,int c)
	{
		int p,np,q,nq;
		p=las;las=np=++sz;mx[np]=mx[p]+1;pos[np]=c;
		for(;p && !ch[p][x];p=fa[p]) ch[p][x]=np;
		if(!p) fa[np]=1;
		else
		{
			q=ch[p][x];
			if(mx[q]==mx[p]+1) fa[np]=q;
			else
			{
				nq=++sz;mx[nq]=mx[p]+1;pos[nq]=pos[q];
				memcpy(ch[nq],ch[q],sizeof(ch[q]));
				fa[nq]=fa[q];fa[q]=fa[np]=nq;
				for(;ch[p][x]==q;p=fa[p]) ch[p][x]=nq;
			}
		}
	}
	void reset(int l)
	{
		memset(ch,0,(sz+2)*sizeof(ch[0]));
		sz=las=1;n=l;
	}
	void buildS(char *s,int l)
	{
		reset(l);
		for(int i=1;i<=n;++i) extend(s[i]-'a',i),tr.insert(rt[las],1,n,i);
		for(int i=1;i<=sz;++i) ++b[mx[i]];
		for(int i=1;i<=n;++i) b[i]+=b[i-1];
		for(int i=1;i<=sz;++i) c[b[mx[i]]--]=i;	
		for(int i=sz,x=c[i];i>1;x=c[--i]) rt[fa[x]]=tr.merge(rt[x],rt[fa[x]]);
	}
	void buildT(char *s,int l)
	{
		reset(l);
		for(int i=1;i<=n;++i) extend(s[i]-'a',i);
	}
}S,T;

void solve()
{
	scanf("%s",s+1);
	int l=read(),r=read(),len=strlen(s+1);
	T.buildT(s,len);memset(lim,0,(len+2)*4);
	for(int i=1,now=0,p=1;i<=len;lim[i++]=now)
	{
		int x=s[i]-'a';
		for(;;)
		{
			if(!tr.query(rt[S.ch[p][x]],1,S.n,l+now,r))
			{
				if(!now) break; --now;
				if(now==S.mx[S.fa[p]]) p=S.fa[p];
			}
			else {++now;p=S.ch[p][x];break;}
		}
	}
	ll ans=0;
	for(int i=2;i<=T.sz;++i) 
		ans+=max(0,T.mx[i]-max(T.mx[T.fa[i]],lim[T.pos[i]]));
	printf("%lld\n",ans);
}

int main()
{
#ifndef ONLINE_JUDGE
	freopen("LGP4770.in","r",stdin);
	freopen("LGP4770.out","w",stdout);
#endif
	scanf("%s",s+1);int len=strlen(s+1);
	S.buildS(s,len);
	for(int T=read();T;--T) solve();
	return 0;
}

【总结】
套路啊套路,本质啊本质。

你可能感兴趣的:(字符串-SAM,数据结构-线段树)