字符串算法总结

(一) 后缀数组(SA)

后缀数组是处理字符串的一大利器,常用倍增算法构造,一般情况下用到Height数组更常见

重要性质

1:两个后缀的最长公共前缀(记为lcp)为两个后缀在排名数组中两点间的Height值得最小值,常配合ST表或单调栈处理
2:将后缀按照字典序排序后,第 i i i个后缀能贡献的本质不同子串个数为 l e n i − H e i g h t i len_i-Height_i leniHeighti,且这些子串是按照字典序排序的
因此可以得到一个字符串的本质不同子串个数为 ∑ i = 1 n l e n i − H e i g h t i \sum_{i=1}^nlen_i-Height_i i=1nleniHeighti,以及求出字典序第 K K K小的子串

常见技巧

1:二分答案,对Height数组分组
Example 1:求不可重叠的,至少重复一次的最长子串

二分答案,发现答案可以表示为两个后缀的lcp,因为在排名数组中区间长度越长,lcp越短,所以将排名数组分成若干组,使得每一组内的Height值都大于mid,这么合法的两个后缀一定在一个组别中,求出这个组别中后缀的最右位置和最左位置,判断是不是是由重叠即可

Example 2: 可重叠的最少重复k次的最长子串

依旧二分答案,并依据答案分组,如果某一组的后缀个数大于等于k,那么这些后缀就符合题意,如果不存在一组就不合法

#include<bits/stdc++.h>
using namespace std;
const int N = 2e6+7;
int s[N],t[3];
int SA[N],Height[N],c[N],x[N],y[N];
int rk[N];
int n,m,len;
void GetSA()
{
	for(int i=1;i<=n;i++) x[i]=s[i];
	for(int i=1;i<=n;i++) c[x[i]]++;
	for(int i=1;i<=m;i++) c[i]+=c[i-1];
	for(int i=n;i>=1;i--) SA[c[x[i]]--]=i;
	for(int k=1;k<=n;k<<=1)
	{
		int tot=0;
		for(int i=n-k+1;i<=n;i++) y[++tot]=i;
		for(int i=1;i<=n;i++) if(SA[i]>k) y[++tot]=SA[i]-k;
		for(int i=1;i<=m;i++) c[i]=0;
		for(int i=1;i<=n;i++) c[x[i]]++;
		for(int i=1;i<=m;i++) c[i]+=c[i-1];
		for(int i=n;i>=1;i--) SA[c[x[y[i]]]--]=y[i];
		swap(x,y);x[SA[1]]=1;tot=1;
		for(int i=2;i<=n;i++)
		if(y[SA[i]]==y[SA[i-1]]&&y[SA[i]+k]==y[SA[i-1]+k]) x[SA[i]]=tot;
		else x[SA[i]]=++tot;
		if(tot==n) break;
		m=tot;
	} 
	for(int i=1;i<=n;i++)
	rk[SA[i]]=i;
	int k=0;
	for(int i=1;i<=n;i++)
	{
		if(rk[i]==1) continue;
		if(k) k--;
		int j=SA[rk[i]-1];
		while(i+k<=n&&j+k<=n&&s[i+k]==s[j+k]) k++;
		Height[rk[i]]=k;
	}
}
bool check(int d)
{
	int cnt=0;
	for(int i=1;i<=n;i++)
	{
		if(Height[i]<d) cnt=0;
		cnt++;
		if(cnt>=len) return 1;	
	}	
	return 0;
}
int main()
{
	cin>>n>>len;
	for(int i=1;i<=n;i++) scanf("%d",&s[i]);
	m=1e6;
	GetSA();
	int l=1,r=n,mid,ans=0;
	while(l<=r)
	{
		mid=(l+r)>>1;
		if(check(mid))
		{
			ans=mid;
			l=mid+1;
		}
		else r=mid-1;
	}
	cout<<ans;
	return 0;	
}
2:将两个串拼接起来求后缀数组
Example 1:求最长文子串

该问题可以用 m a n a c h a r manachar manachar O ( n ) O(n) O(n)的时间内解决,这里采用后缀数组解决
分成奇回文和偶回文讨论,这里以奇回文为例
枚举回文中心,相当于是求向左和向右分别最远能扩展的距离,使得这两段相同,相当于是求,这个前缀和这个后缀的lcp,可以考虑爆把原串翻转一下拼在后面,为了区分前后要在中间加入一个其他字符

#include<bits/stdc++.h>
using namespace std;
const int N = 4e5+7;
char s[N];
int SA[N],Height[N],c[N],x[N],y[N];
int rk[N];
int n,m;
void GetSA()
{
	for(int i=1;i<=n;i++) x[i]=s[i];
	for(int i=1;i<=n;i++) c[x[i]]++;
	for(int i=1;i<=m;i++) c[i]+=c[i-1];
	for(int i=n;i>=1;i--) SA[c[x[i]]--]=i;
	for(int k=1;k<=n;k<<=1)
	{
		int tot=0;
		for(int i=n-k+1;i<=n;i++) y[++tot]=i;
		for(int i=1;i<=n;i++) if(SA[i]>k) y[++tot]=SA[i]-k;
		for(int i=1;i<=m;i++) c[i]=0;
		for(int i=1;i<=n;i++) c[x[i]]++;
		for(int i=1;i<=m;i++) c[i]+=c[i-1];
		for(int i=n;i>=1;i--) SA[c[x[y[i]]]--]=y[i];
		swap(x,y);x[SA[1]]=1;tot=1;
		for(int i=2;i<=n;i++)
		if(y[SA[i]]==y[SA[i-1]]&&y[SA[i]+k]==y[SA[i-1]+k]) x[SA[i]]=tot;
		else x[SA[i]]=++tot;
		if(tot==n) break;
		m=tot;
	} 
	for(int i=1;i<=n;i++)
	rk[SA[i]]=i;
	int k=0;
	for(int i=1;i<=n;i++)
	{
		if(rk[i]==1) continue;
		if(k) k--;
		int j=SA[rk[i]-1];
		while(i+k<=n&&j+k<=n&&s[i+k]==s[j+k]) k++;
		Height[rk[i]]=k;
	}
}
int st[N][30];
int t=0;
void build()
{
	for(int i=1;i<=n;i++)
	st[i][0]=Height[i];
	for(int k=1;k<=t;k++)
	for(int i=1;i+(1<<(k))-1<=n;i++)
	st[i][k]=min(st[i][k-1],st[i+(1<<(k-1))][k-1]);
}
int pos[N],Left[N];
int len=0,ans=0;
int ask(int l,int r)
{
	if(l>r) swap(l,r);
	if(l==0||r==0) return 0;
	l++;
	int k=log2(r-l+1);
	return min(st[l][k],st[r-(1<<k)+1][k]);
}
int main()
{
	freopen("circle.in","r",stdin);
	freopen("circle.out","w",stdout);
	scanf("%s",s+1);
	n=strlen(s+1);
	s[n+1]='$';
	for(int i=1;i<=n;i++)
	s[n+1+i]=s[n-i+1];
	n=2*n+1;
	m=122;
	GetSA();
	t=log2(n)+1;
	build();
	for(int i=1;i<=(n-1)/2;i++)
	{
		pos[i]=ask(rk[n-i+1],rk[i]);
		if(i>1) Left[i]=ask(rk[n-i+2],rk[i]);
	}
	for(int i=1;i<=(n-1)/2;i++)
	{
		if(len<pos[i]*2-1) 
		{
			len=pos[i]*2-1;
			ans=i-pos[i]+1;
		}
		if(i>1&&len<Left[i]*2)
		{
			len=Left[i]*2;
			ans=i-Left[i];
		}
	}
	for(int i=1;i<=len;i++)
	cout<<s[ans+i-1];
	return 0;
}
Example 2:求两个串的最长公共子串

将两个字符串拼接起来,求出后缀数组,发现一定有相邻的两个后缀满足来自不同字符串,而其他的后缀因为离的更远,所以答案一定不会更优,所以答案就是排名相邻的、来自不同字符串的后缀的 h e i g h t height height的最大值

#include<bits/stdc++.h>
using namespace std;
const int N = 4e5+7;
char s[N],t[N];
int SA[N],Height[N],c[N],x[N],y[N];
int rk[N];
int n,m;
void GetSA()
{
	for(int i=1;i<=n;i++) x[i]=s[i];
	for(int i=1;i<=n;i++) c[x[i]]++;
	for(int i=1;i<=m;i++) c[i]+=c[i-1];
	for(int i=n;i>=1;i--) SA[c[x[i]]--]=i;
	for(int k=1;k<=n;k<<=1)
	{
		int tot=0;
		for(int i=n-k+1;i<=n;i++) y[++tot]=i;
		for(int i=1;i<=n;i++) if(SA[i]>k) y[++tot]=SA[i]-k;
		for(int i=1;i<=m;i++) c[i]=0;
		for(int i=1;i<=n;i++) c[x[i]]++;
		for(int i=1;i<=m;i++) c[i]+=c[i-1];
		for(int i=n;i>=1;i--) SA[c[x[y[i]]]--]=y[i];
		swap(x,y);x[SA[1]]=1;tot=1;
		for(int i=2;i<=n;i++)
		if(y[SA[i]]==y[SA[i-1]]&&y[SA[i]+k]==y[SA[i-1]+k]) x[SA[i]]=tot;
		else x[SA[i]]=++tot;
		if(tot==n) break;
		m=tot;
	} 
	for(int i=1;i<=n;i++)
	rk[SA[i]]=i;
	int k=0;
	for(int i=1;i<=n;i++)
	{
		if(rk[i]==1) continue;
		if(k) k--;
		int j=SA[rk[i]-1];
		while(i+k<=n&&j+k<=n&&s[i+k]==s[j+k]) k++;
		Height[rk[i]]=k;
	}
}
int main()
{
	scanf("%s",s+1);
	n=strlen(s+1);
	s[++n]='$';
	scanf("%s",t+1);
	int l=strlen(t+1);
	for(int i=1;i<=l;i++)
	s[++n]=t[i];
	m=122;
	GetSA();
	int ans=0;
	for(int i=2;i<=n;i++)
	{
		int x=SA[i],y=SA[i-1];	
		if(x==n-l||y==n-l||(x<(n-l)&&y<(n-l))||((x>(n-l)&&y>(n-l)))) continue;
		ans=max(ans,Height[i]);
	}	
	cout<<ans;
	return 0;
}
Example 3:求n串的最长公共子串

把这些串拼起来,求后缀数组
二分答案,根据答案对后缀分组,如果某一组内出现了所有不同字符串,则表示可行

进阶题目

Example 1:[NOI2015] 品酒大会

考虑第一问,第二问类似
我们只需要对于每个定值R的R相似酒的个数即可,然后做一遍后缀和就行了
很显然如果按照R对Height分组,那么只有同一组的酒可能成为答案,贡献为 C c n t 2 C_{cnt}^2 Ccnt2,cnt为组的大小,那么考虑按照Height从打到小加入每个后缀,用并查集维护极大连通块的大小,然后加上合并两个联通块所产生的答案,就可以得到每个r的值

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1e6+7;
const LL INF = 2e18+20;
int cnt[N],fst[N],sec[N],sa[N],height[N],rk[N],f[N],siz[N],w[N];
LL max1[N],max2[N],min1[N],min2[N],Cnt[N],Max[N];
vector<int> h[N];
int n,m;
char s[N];
void get_sa()
{
	for(int i=1;i<=n;i++)
	{
		fst[i]=s[i];
		cnt[fst[i]]++;
	}
	for(int i=2;i<=m;i++)
	cnt[i]+=cnt[i-1];
	for(int i=n;i>=1;i--)
	{
		sa[cnt[fst[i]]]=i;
		cnt[fst[i]]--;	
	}
	for(int k=1;k<=n;k<<=1)
	{
		int num=0;
		for(int i=n-k+1;i<=n;i++)
		sec[++num]=i;
		for(int i=1;i<=n;i++)
		{
			if(sa[i]-k>0)
			sec[++num]=sa[i]-k;
		}
		for(int i=1;i<=m;i++) cnt[i]=0;
		for(int i=1;i<=n;i++) cnt[fst[i]]++;
		for(int i=2;i<=m;i++) cnt[i]+=cnt[i-1];
		for(int i=n;i>=1;i--)
		{
			sa[cnt[fst[sec[i]]]]=sec[i];
			cnt[fst[sec[i]]]--;
			sec[i]=0;
		}
		swap(sec,fst);
		fst[sa[1]]=1,num=1;
		for(int i=2;i<=n;i++)
		{
			if(sec[sa[i]]==sec[sa[i-1]]&&sec[sa[i]+k]==sec[sa[i-1]+k])
			fst[sa[i]]=num;
			else fst[sa[i]]=++num;
		}
		if(num==n) break;
		m=num;
	}
}
void get_height()
{
	for(int i=1;i<=n;i++)
	rk[sa[i]]=i;
	for(int i=1,k=0;i<=n;i++)
	{
		if(rk[i]==1) continue;
		if(k) k--;
		int j=sa[rk[i]-1];
		while(i+k<=n&&j+k<=n&&s[i+k]==s[j+k]) k++;
		height[rk[i]]=k;
	}
}
int Find(int x)
{
	if(x==f[x]) return x;
	return f[x]=Find(f[x]);
}
LL C(int n)
{
	return n*(n-1ll)/2;
}
void merge(int r)
{
	LL cnt=0,maxx=-INF;
	for(int i=0;i<h[r].size();i++)
	{
		int a=h[r][i],b=a-1;
		int x=Find(a),y=Find(b);
		cnt=cnt-(C(siz[x])+C(siz[y]));
		f[x]=y;
		siz[y]+=siz[x];
		cnt+=C(siz[y]);
		if(max1[x]>=max1[y]) max2[y]=max(max1[y],max2[x]),max1[y]=max1[x];
		else if(max1[x]>max2[y]) max2[y]=max1[x];
		if(min1[x]<=min1[y]) min2[y]=min(min1[y],min2[x]),min1[y]=min1[x];
		else if(min1[x]<min2[y]) min2[y]=min1[x];
		maxx=max(maxx,max(max1[y]*max2[y],min1[y]*min2[y]));
	}
	if(maxx==-INF) Max[r]=0;
	else if(Max[r+1]!=0) Max[r]=max(maxx,Max[r+1]);
	else Max[r]=maxx;
	Cnt[r]=Cnt[r+1]+cnt;
}
int main()
{
	cin>>n;
	scanf("%s",s+1);
	for(int i=1;i<=n;i++)
	scanf("%d",&w[i]);
	m=122;
	get_sa();
	get_height();
	for(int i=1;i<=n;i++)
	{
		siz[i]=1;
		f[i]=i;
		max1[i]=min1[i]=w[sa[i]];
		max2[i]=-INF;
		min2[i]=INF;
	}
	Max[n]=-INF;
	for(int i=2;i<=n;i++)
	h[height[i]].push_back(i);	
	for(int i=n-1;i>=0;i--) merge(i);
	for(int i=0;i<=n-1;i++)
	printf("%lld %lld\n",Cnt[i],Max[i]);
	return 0;
} 
Example 2:[SDOI2016]生成魔咒

将串反过来,这样可以看成在字符串前加入一个后缀,但是需要动态维护Height数组,这并不好做
考虑倒过来,将添加变成删除,可以用链表维护每个后缀排名前面的一个后缀,然后删除一个后缀的时候影响的只有排名下一个的后缀,我们也可以得到新的Height=lcp(i-1,i+1),并更新答案,双向链表维护即可

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1e5+7;
int dct[N],num=0;
bool cmp(int x,int y)
{
	return x<y;
}
int s[N],t[N],c[N],SA[N],Height[N],nxt[N],pre[N];
int rk[N];
int n,m;
int x[N],y[N];
void GetSA()
{
	for(int i=1;i<=n;i++) x[i]=s[i];
	for(int i=1;i<=n;i++) c[x[i]]++;
	for(int i=1;i<=m;i++) c[i]+=c[i-1];
	for(int i=1;i<=n;i++) SA[c[x[i]]--]=i;
	for(int k=1;k<=n;k<<=1)
	{
		int tot=0;
		for(int i=n-k+1;i<=n;i++)
		y[++tot]=i;
		for(int i=1;i<=n;i++)
		if(SA[i]>k) y[++tot]=SA[i]-k;
		for(int i=1;i<=m;i++) c[i]=0;
		for(int i=1;i<=n;i++) c[x[i]]++;
		for(int i=1;i<=m;i++) c[i]+=c[i-1];
		for(int i=n;i>=1;i--) SA[c[x[y[i]]]--]=y[i];
		swap(x,y);
		x[SA[1]]=1;tot=1;
		for(int i=2;i<=n;i++)
		{
			if(y[SA[i]]==y[SA[i-1]]&&y[SA[i]+k]==y[SA[i-1]+k]) x[SA[i]]=tot;
			else x[SA[i]]=++tot;
		}
		if(tot==n) break;
		m=tot;
	}
}
void GetHeight()
{
	for(int i=1;i<=n;i++)
	rk[SA[i]]=i;
	for(int i=1,k=0;i<=n;i++)
	{
		if(rk[i]==1) continue;
		if(k) k--;
		int j=SA[rk[i]-1];
		while(i+k<=n&&j+k<=n&&s[i+k]==s[j+k]) k++;
		Height[rk[i]]=k;
	} 
}
void Build()
{
	pre[SA[1]]=-1;
	nxt[SA[1]]=SA[2];
	pre[SA[n]]=SA[n-1];
	nxt[SA[n]]=-1; 
	for(int i=2;i<n;i++)
	{
		pre[SA[i]]=SA[i-1];
		nxt[SA[i]]=SA[i+1];
	}
}
LL Ans(int i)
{
	return (n-SA[i]+1)-Height[i];
}
LL ans[N];
LL sum=0;
int main()
{
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		scanf("%d",&s[i]);
		dct[++num]=s[i];
	}
	sort(dct+1,dct+num+1,cmp);
	int len=unique(dct+1,dct+num+1)-dct-1;
	for(int i=1;i<=n;i++)
	s[i]=lower_bound(dct+1,dct+len+1,s[i])-dct;
	for(int i=1;i<=n;i++)
	t[i]=s[i];
	for(int i=1;i<=n;i++)
	s[i]=t[n-i+1];
	m=n;
	GetSA();
	GetHeight();
	Build();	
	for(int i=1;i<=n;i++)
	sum+=Ans(i);
	for(int i=1;i<=n;i++)
	{
		ans[i]=sum;
		sum-=Ans(rk[i]);
		if(nxt[i]!=-1)	
		{
			sum-=Ans(rk[nxt[i]]);
			Height[rk[nxt[i]]]=min(Height[rk[nxt[i]]],Height[rk[i]]);
			sum+=Ans(rk[nxt[i]]);
			nxt[pre[i]]=nxt[i];
			pre[nxt[i]]=pre[i];
		}
		else nxt[pre[i]]=-1;
	}
	for(int i=n;i>=1;i--)
	printf("%lld\n",ans[i]);
	return 0;
}
Example 3:[BZOJ 4310]跳蚤

我们二分答案串的字典序排名,并求出排名mid的子串
问题转化为能否将原串分成小于等于K段,使得每一个部分的字典序最大的子串的字典序排名小于等于mid,贪心的考虑,从后往前划分,我们发现一个子串的字典序一定没有这个子串属于的后缀的字典序大,所以如果我们当前的位置到这一段的结束位置的后缀子串的字典序大于mid,那么这个位置就必须新建一个段,判断最终段的个数是否小于等于mid就行了

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const LL N = 2e5+7;
LL n,m,K;
LL c[N],x[N],y[N];
char s[N];
LL SA[N],Height[N],rk[N];
void GetSA()
{
	for(int i=1;i<=n;i++) x[i]=s[i];
	for(int i=1;i<=n;i++) c[x[i]]++;
	for(int i=1;i<=m;i++) c[i]+=c[i-1];
	for(int i=n;i>=1;i--) SA[c[x[i]]--]=i;
	for(int k=1;k<=n;k<<=1)
	{
		int tot=0;
		for(int i=n-k+1;i<=n;i++) y[++tot]=i;
		for(int i=1;i<=n;i++) if(SA[i]>k) y[++tot]=SA[i]-k;
		for(int i=1;i<=m;i++) c[i]=0;
		for(int i=1;i<=n;i++) c[x[i]]++;
		for(int i=1;i<=m;i++) c[i]+=c[i-1];
		for(int i=n;i>=1;i--) SA[c[x[y[i]]]--]=y[i];
		swap(x,y);x[SA[1]]=1;tot=1;
		for(int i=2;i<=n;i++)
		if(y[SA[i]]==y[SA[i-1]]&&y[SA[i]+k]==y[SA[i-1]+k]) x[SA[i]]=tot;
		else x[SA[i]]=++tot;
		if(tot==n) break;
		m=tot;
	} 
	for(int i=1;i<=n;i++)
	rk[SA[i]]=i;
	int k=0;
	for(int i=1;i<=n;i++)
	{
		if(rk[i]==1) continue;
		if(k) k--;
		int j=SA[rk[i]-1];
		while(i+k<=n&&j+k<=n&&s[i+k]==s[j+k]) k++;
		Height[rk[i]]=k;
	}
}
LL f[N][30],t;
void build()
{
	for(LL i=1;i<=n;i++)
	f[i][0]=Height[i];
	for(LL k=1;k<=t;k++)
	for(LL i=1;i+(1<<k)-1<=n;i++)
	f[i][k]=min(f[i][k-1],f[i+(1<<(k-1))][k-1]);
}
LL lcp(LL l,LL r)
{
	if(l>r) swap(l,r);
	if(l==0) return 0;
	l++;
	LL k=log2(r-l+1);
	return min(f[l][k],f[r-(1<<k)+1][k]);
}
bool cmp(LL l1,LL r1,LL l2,LL r2)
{
	LL s1=r1-l1+1,s2=r2-l2+1;
	if(l1==l2) return s1<=s2;
	LL len=lcp(rk[l1],rk[l2]);
	if(len>=s1||len>=s2) return s1<=s2;
	return s[l1+len]<s[l2+len]; 
}
LL val(LL x)
{
	return n-SA[x]+1-Height[x];
}
LL L=0,R=0;
LL ansl=0,ansr=0;
void kth(LL k)
{
	
	for(LL i=1;i<=n;i++)
	{
		if(k>val(i)) k-=val(i);
		else
		{
			L=SA[i];
			R=SA[i]+Height[i]+k-1;			
			return;
		}
	}
}
bool check()
{
	LL last=n,cnt=1;
	for(LL i=n;i>=1;i--)
	{
		if(s[i]>s[L]) return 0;
		if(cmp(i,last,L,R)==0)
		{
			cnt++;
			last=i;
		}
	}
	if(cnt<=K) return 1;
	return 0;
}
int main()
{
	scanf("%lld",&K);
	scanf("%s",s+1);
	n=strlen(s+1);
	m=122;
	GetSA();
	t=log2(n)+1;
	build(); 
	LL l=1,r=0,mid;
	for(LL i=1;i<=n;i++)
	r+=val(i);
	while(l<=r)
	{
		mid=(l+r)>>1ll;
		kth(mid);
		if(check())
		{
			ansl=L;
			ansr=R;
			r=mid-1;
		}
		else l=mid+1;
	}
	for(LL i=ansl;i<=ansr;i++)
	putchar(s[i]);
	return 0;
} 

Example 4:[十二省联考 2019] 字符串问题

题意:给定字符串S和两类区间,以及一些从第一类区间连详解第二类区间的边,第二类区间可以连向第一类区间当且仅当前者是后者的前缀,每个第一类区间的价值是区间长度,第二类区间价值为0,现在希望将若干有边的区间拼合起来,使得最终字符串的价值和尽可能长,如果可以无限长输出-1

如果有环则是-1,现在要考虑的是怎么优化建图

我们建立后缀数组,那么对于每个第二类区间,某个第一类区间可以和它右边当且仅当这两个区间属于的后缀的lcp,大于等于这个第二类区间的长度,而在后缀数组中这很显然是一段连续区间,我们可以倍增求出,然后直接上线段树优化建图就行了

但是还有个长度限制,也就是如果一类串长度小于第二类串,则肯定不能连边

也就是说我们要向某个区间内长度大于等于某个值的第一类区间连边,我们可以联想到主席树

但是这相当于主席树上区间修改,我并不会,这里采用另一种更巧妙的做法

将A类串排序,从大到小加入每个串到主树中,然后对于这个B类串,当前主席树维护的就是长度大于等于它长度的串,直接找到区间连边就行了,最后拓扑DP求最长路

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 200005;
const int M = 4200005;
char s[N];
int SA[N],Height[N],c[N],x[N],y[N];
int rk[N];
int n,m;
void GetSA()
{
	m=122;
	memset(x,0,sizeof(x));
	memset(y,0,sizeof(y));
	memset(c,0,sizeof(c));
	for(int i=1;i<=n;i++) x[i]=s[i];
	for(int i=1;i<=n;i++) c[x[i]]++;
	for(int i=1;i<=m;i++) c[i]+=c[i-1];
	for(int i=n;i>=1;i--) SA[c[x[i]]--]=i;
	for(int k=1;k<=n;k<<=1)
	{
		int tot=0;
		for(int i=n-k+1;i<=n;i++) y[++tot]=i;
		for(int i=1;i<=n;i++) if(SA[i]>k) y[++tot]=SA[i]-k;
		for(int i=1;i<=m;i++) c[i]=0;
		for(int i=1;i<=n;i++) c[x[i]]++;
		for(int i=1;i<=m;i++) c[i]+=c[i-1];
		for(int i=n;i>=1;i--) SA[c[x[y[i]]]--]=y[i];
		swap(x,y);x[SA[1]]=1;tot=1;
		for(int i=2;i<=n;i++)
		if(y[SA[i]]==y[SA[i-1]]&&y[SA[i]+k]==y[SA[i-1]+k]) x[SA[i]]=tot;
		else x[SA[i]]=++tot;
		if(tot==n) break;
		m=tot;
	} 
	for(int i=1;i<=n;i++)
	rk[SA[i]]=i;
	int k=0;
	for(int i=1;i<=n;i++)
	{
		if(rk[i]==1) continue;
		if(k) k--;
		int j=SA[rk[i]-1];
		while(i+k<=n&&j+k<=n&&s[i+k]==s[j+k]) k++;
		Height[rk[i]]=k;
	}
}
int Log[N];
void pre(int n)
{
	for(int i=2;i<=n;i++) 
	Log[i]=Log[i>>1]+1;
}
int st[N][30];
void Build()
{
	for(int i=2;i<=n;i++)
	st[i][0]=Height[i];
	for(int k=1;k<=Log[n];k++)
	{
		for (int i=1;i<=(1<<k);i++) 
		st[i][k]=0;
		for (int i=(1<<k|1);i<=n;i++)
		st[i][k]=min(st[i-(1<<(k-1))][k-1],st[i][k-1]);
	}
}
int A,L1[N],R1[N];
int B,lb[N],rb[N];
struct Substr
{
	int rk,len,typ,id;
	Substr() 
	{
		rk=0;
		len=0;
		typ=0;
		id=0;
	}
	void Sub(int r,int l,int t,int p)
	{
		rk=r;
		len=l;
		typ=t;
		id=p;
	}
}sub[N*2];
bool cmp(Substr a,Substr b)
{
	if(a.len==b.len) return a.typ<b.typ;
	return a.len>b.len;
}
int deg[M];
vector<int> G[M];
void add(int x,int y) 
{
 	++deg[y]; 
 	G[x].push_back(y); 
}
int rot[N],lson[M],rson[M],val[M],cnt;
void Modify(int &k,int l,int r,int p,int x) 
{
	lson[++cnt]=lson[k],rson[cnt]=rson[k];
	if(k) add(cnt,k);
	k=cnt;
	val[k]=0;
	if(l==r) 
	{ 
		add(k,x); 
		return; 
	}
	int mid=(l+r)>>1;
	if(p<=mid) 
	{
		Modify(lson[k],l,mid,p,x);
		add(k, lson[k]);
	}
	else 
	{
		Modify(rson[k],mid+1,r,p,x);
		add(k,rson[k]);
	}
}
void Add(int rot,int l,int r,int L,int R,int x) 
{
	if (!rot) return;
	if(L<=l&&r<=R) 
	{ 
		add(x,rot); 
		return; 
	}
	int mid=(l+r)>>1;
	if(L<=mid) Add(lson[rot],l,mid,L,R,x);
	if(R>mid) Add(rson[rot],mid+1,r,L,R,x);
}
queue<int> q;
LL f[M];
inline int read()
{
	int X=0; bool flag=1; char ch=getchar();
	while(ch<'0'||ch>'9') {if(ch=='-') flag=0; ch=getchar();}
	while(ch>='0'&&ch<='9') {X=(X<<1)+(X<<3)+ch-'0'; ch=getchar();}
	if(flag) return X;
	return ~(X-1);
}
void solve()
{
	scanf("%s",s+1);
	n=strlen(s+1);
	GetSA();
	Build();
	A=read();
	for(int i=1;i<=A;i++)
	{
		L1[i]=read();
		R1[i]=read();
		sub[i].Sub(rk[L1[i]],R1[i]-L1[i]+1,0,i);
	}
	B=read();
	for(int i=1;i<=B;i++)
	{
		lb[i]=read();
		rb[i]=read();
		sub[A+i].Sub(rk[lb[i]],rb[i]-lb[i]+1,1,i);		
	}
	sort(sub+1,sub+A+B+1,cmp);
	cnt=A+B;
	for(int i=1;i<=A;i++) 
	val[i]=R1[i]-L1[i]+1;
	for(int i=1;i<=B;i++) 
	val[A+i]=0;
	for(int i=1,now=0;i<=A+B;i++) 
	{
		Substr p=sub[i];
		if(!p.typ) 
		{
			++now;
			rot[now]=rot[now-1];
			Modify(rot[now],1,n,p.rk,p.id);
		}
		else
		{
			int L=p.rk,R=p.rk;
			for(int j=Log[p.rk-1];j>=0;j--)
			if(st[L][j]>=p.len) L-=(1<<j);
			for(int j=Log[n-p.rk];j>=0;j--)
			if(R+(1<<j)<=n&&st[R+(1<<j)][j]>=p.len) R+=1<<j;
			Add(rot[now],1,n,L,R,A+p.id);
		}
	}
	int R=read(); 
	while(R--) 
	{
		int i=read(),j=read();
		add(i,A+j);
	}
	LL Ans=0;
	for(int i=1;i<=cnt;i++) 
	{
		f[i]=val[i];
		if(!deg[i]) q.push(i);
	}
	int tim=0;
	while(!q.empty())
	{
		int x=q.front();
		tim++;
		q.pop();
		Ans=max(Ans,f[x]);
		for(int i=0;i<G[x].size();i++)
		{
			int y=G[x][i];
			f[y]=max(f[y],f[x]+val[y]);
			deg[y]--;
			if(deg[y]==0) q.push(y);
		}
	}
	if(tim!=cnt) printf("-1\n");
	else printf("%lld\n",Ans);
	for(int i=1;i<=cnt;i++) 
	{
		deg[i]=0;
		G[i].clear();	
	}	
}
int main()
{
	int T=read(); 
	pre(200000);
	while(T--)
	{
		solve();
	}
	return 0;
}

后缀自动机(SAM)

非常热门的字符串自动机,套路非常多

重要性质

1:每个点所对应的Endpos等价类的子串个数为 l e n i − l e n f a i len_i-len_{fa_i} lenilenfai,加起来记为总共的本质不同的子串个数
2:后缀自动机是一个DAG,可以配合Dp使用
3:后缀自动机的parent树,代表了原串的所有子串,且每一个点的儿子的出现位置是无交的,且共同构成了这个点的Endpos,当然如果是原串的前缀的话就是还有一个位置是他自己,因为这个时候他没有儿子,因为他不能往前面添加字符,基于这个性质可以配合多种树上数据结构食用

例题

Example 1:求若干串的最长公共子串

先考虑两个串,我们对一个串建SAM,然后让另一个串在SAM上跑,求出每个前缀能匹配的最长后缀,并在SAM上取max,最终所有点的答案再取max
那么N个字符串的时候,对于每个串都做一次上述过程,对于每个点,因为要满足所有串的限制,所有要再取min,最后求出所有点的max

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1e5+7;
char s[N];
LL f[N];
int tr[N][26],fa[N],len[N];
int n,m;
int tot=1,last=1;
void copy(int a,int b)
{
	fa[a]=fa[b];
	len[a]=len[b];
	for(int i=0;i<26;i++)
	tr[a][i]=tr[b][i];
}
void Extend(int c)
{
	int p=last,np=last=++tot;
	len[np]=len[p]+1;
	f[np]=1;
	while(p&&!tr[p][c]) 
	{
		tr[p][c]=np;
		p=fa[p];
	}
	if(!p) fa[np]=1;
	else 
	{
		int q=tr[p][c];
		if(len[q]==len[p]+1) fa[np]=q;
		else
		{
			int nq=++tot;
			copy(nq,q);
			len[nq]=len[p]+1;
			fa[np]=fa[q]=nq;
			while(p&&tr[p][c]==q) 
			{
				tr[p][c]=nq;
				p=fa[p];
			}
		}
	}
}
int ans[N],cnt[N];
int main()
{
	freopen("lcs.in","r",stdin);
	freopen("lcs.out","w",stdout);
	cin>>m;
	scanf("%s",s+1);
	n=strlen(s+1);
	for(int i=1;i<=n;i++) Extend(s[i]-'a');
	for(int i=1;i<=tot;i++) ans[i]=len[i]; 
	m--;
	while(m--)
	{
		scanf("%s",s+1);
		n=strlen(s+1);
		memset(cnt,0,sizeof(cnt));
		int p=1,t=0;
		for(int i=1;i<=n;i++)
		{
			int c=s[i]-'a';
			while(p>1&&!tr[p][c])
			{
				p=fa[p];
				t=len[p];
			}
			if(tr[p][c]) p=tr[p][c],t++;
			cnt[p]=max(cnt[p],t);
		}
		for(int i=1;i<=tot;i++)
		ans[i]=min(ans[i],cnt[i]);
	}
	int res=0;
	for(int i=1;i<=tot;i++)
	res=max(res,ans[i]);
	cout<<res;
	return 0;
}

Example 2:查找原串的某一个子串在SAM中的位置

子串以 l l l r r r的形式给出
我们先找到r对应前缀的节点,考虑一个性质,从某个点跳parent,那么每次会从这个子串最前面删掉字符并且长度越来越小,也就是说一开始昌都市r,我们需要往parent上跳,直到某一个点代表的最长子串小于r-l+1,这个过程可以用树上倍增实现

Example 3:查找字典序第K小子串

[TJOI2015]弦论
首先先对原串建出后缀自动机,然后可以用拓扑序,求出包含某个子串的子串个数,也就是倒叙进行拓扑,累加路径条数就行了,求答案的时候从起点开始走,从字典序最小的边开始走,判断包含这个子串的子串个数是不是大于K,如果是,说明应该从这个边走,否则的话就让K减掉还包含这个子串的子串个数,一直这样走就能得到答案,如果要求本质不同则在求路径条数的时候初值设为1就行了

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1e6+7;
char s[N];
int fa[N],len[N],tr[N][26];
int n;
int tot=1,last=1;
LL f[N],sum[N];
void copy(int a,int b)
{
	fa[a]=fa[b];
	len[a]=len[b];
	for(int i=0;i<26;i++)
	tr[a][i]=tr[b][i];
}
void Extend(int c)
{
	int p=last,np=last=++tot;
	len[np]=len[p]+1;
	f[np]=1;
	while(p&&!tr[p][c]) 
	{
		tr[p][c]=np;
		p=fa[p];
	}
	if(!p) fa[np]=1;
	else 
	{
		int q=tr[p][c];
		if(len[q]==len[p]+1) fa[np]=q;
		else
		{
			int nq=++tot;
			copy(nq,q);
			len[nq]=len[p]+1;
			fa[np]=fa[q]=nq;
			while(p&&tr[p][c]==q) 
			{
				tr[p][c]=nq;
				p=fa[p];
			}
		}
	}
}
struct edge
{
	int y,next;
}e[N];
int link[N],t=0;
void add(int x,int y)
{
	e[++t].y=y;
	e[t].next=link[x];
	link[x]=t;
}
int T;
void dfs(int x)
{
	for(int i=link[x];i;i=e[i].next)
	{
		int y=e[i].y;
		dfs(y);
		f[x]+=f[y];
	}
}
int k;
void print(int x)
{
	if(k<=f[x]) return;
	k-=f[x];
	for(int c=0;c<26;c++)
	{
		int y=tr[x][c];
		if(!y) continue;
		if(k>sum[y]) k-=sum[y];
		else
		{
			putchar(c+'a');
			print(y);
			return;
		}
	}
}
bool vis[N];
void Exdfs(int x)
{
	if(vis[x]) return;
	vis[x]=1;
	for(int i=0;i<26;i++)
	{
		int y=tr[x][i];
		if(!y) continue;
		Exdfs(y);
		sum[x]+=sum[y];
	}
}
int main()
{
	scanf("%s",s+1);
	n=strlen(s+1);
	for(int i=1;i<=n;i++) Extend(s[i]-'a');
	for(int i=2;i<=tot;i++) add(fa[i],i);
	scanf("%d %d",&T,&k);
	dfs(1);
	for(int i=1;i<=tot;i++)
	sum[i]=T?f[i]:(f[i]=1);
	sum[1]=f[1]=0;
	Exdfs(1);	
	if(sum[1]<k) cout<<-1;
	else print(1);
	return 0;
}

Example 4:[HAOI2016]找相同字符

题意是求两个字符串的公共子串个数,不要求本质不同

后缀数组做法:

将两个串拼起来求后缀数组,如果把Height值看成矩形,那么这个题就是求矩形两侧属于不同字符串的矩形个数,可以用单调栈维护一个上升的轮廓线,当新加入一个后缀时,与栈中的每个元素都会形成矩形,形成举行的个数就是元素大小

后缀自动机做法:

这个就很简单了,将两个串拼起来建后缀自动机,那么对于每个节点的每个子串,从出现位置中任选两个就能得到答案,出现位置可以用parent树递推得到,这样就得到了从这个拼接的字符串中选出两个子串,使得这两个子串相同的方案数,我们再对两个字符串分别重复上述过程,用总的答案减掉在两个字符串中的答案就是分属于不同字符串的答案

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 8e5;
char s[N],ss[N];
int fa[N],tr[N][26];
LL len[N];
int n,m;
int tot=1,last=1;
LL f[N];
void copy(int a,int b)
{
	fa[a]=fa[b];
	len[a]=len[b];
	for(int i=0;i<26;i++)
	tr[a][i]=tr[b][i];
}
void Extend(int c)
{
	int p=last,np=last=++tot;
	len[np]=len[p]+1;
	f[np]=1;
	while(p&&!tr[p][c]) 
	{
		tr[p][c]=np;
		p=fa[p];
	}
	if(!p) fa[np]=1;
	else 
	{
		int q=tr[p][c];
		if(len[q]==len[p]+1) fa[np]=q;
		else
		{
			int nq=++tot;
			copy(nq,q);
			len[nq]=len[p]+1;
			fa[np]=fa[q]=nq;
			while(p&&tr[p][c]==q) 
			{
				tr[p][c]=nq;
				p=fa[p];
			}
		}
	}
}
struct edge
{
	int y,next;
}e[N];
int link[N],t=0; 
void add(int x,int y)
{
	e[++t].y=y;
	e[t].next=link[x];
	link[x]=t;
}
void clear()
{
	last=1;
	tot=1;
	memset(f,0,sizeof(f));
	memset(tr,0,sizeof(tr));
	memset(fa,0,sizeof(fa));
	memset(len,0,sizeof(len));
	memset(link,0,sizeof(link));
	t=0;
}
LL ans=0,res=0;
void Build()
{
	for(int i=2;i<=tot;i++)
	add(fa[i],i);
}
LL opt=1;
void Getans(int x)
{
	for(int i=link[x];i;i=e[i].next)
	{
		int y=e[i].y;
		Getans(y);
		f[x]+=f[y];
	}
	res=res+(opt)*(1ll*f[x]*(f[x]-1ll)*(len[x]-len[fa[x]])>>1ll);
}
int main()
{
	scanf("%s",s+1); n=strlen(s+1);
	for(int i=1;i<=n;i++) Extend(s[i]-'a');  opt=-1ll;
	Build(); Getans(1);
	clear();
	scanf("%s",ss+1); m=strlen(ss+1);
	for(int i=1;i<=m;i++) Extend(ss[i]-'a'); opt=-1ll;
	Build(); Getans(1);
	clear();
	s[++n]='$';
	for(int i=1;i<=m;i++)
	s[++n]=ss[i];
	for(int i=1;i<=n;i++) Extend(s[i]-'a'); opt=1ll;
	Build(); Getans(1); 
	cout<<res;
	return 0;
}

广义后缀自动机

我们可以将后缀自动机看成是依据一个中心串构造的,这个中心串也就是原串
而所谓的广义后缀自动机则可以使看成现将原来的串建出一颗trie树,然后以这个trie树为中心建出后缀自动机,那么我们会发现建出的广义后缀自动机保留了各个串的信息
比如说在这n个串中出现的本质不同子串个数就直接在广义SAM上按照原来的方法求就行了

Example 1:[BZOJ3277]串

我们把每个串在广义SAM上跑一遍,求出每个子串在几个字符串中出现的次数
至于怎么跑,就是对于每个字符串,枚举每一个前缀,并找到在SAM中的位置,那么这个前缀的所有后缀一定也在这个串中出现,所以把这个点的所有father也都更新,因为更新到一个已经被更新过的点就不用再更新了,这个复杂度大致为 O ( n n ) O(n\sqrt n) O(nn ),具体证明可参考这里
f i f_i fi表示某个节点及其后缀至少在K个母串中出现的后缀个数
f i = f f a i + ( c n t i > = K ) × ( l e n i − l e n f a i ) f_i=f_{fa_i}+(cnt_i>=K)\times(len_i-len_{fa_i}) fi=ffai+(cnti>=K)×(lenilenfai)
然后我们对于每个母串,再在SAM上跑一遍,把它的每个前缀的f值累加一遍,因为不要求本质不同,所以这就是答案
时间复杂度也为 O ( n n ) O(n\sqrt n) O(nn )

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 2e5+7;
int tr[N][26],fa[N],len[N];
int tot=1;
int trie[N][26];
int n,m;
char s[N];
vector<char> str[N];
void Insert()
{
	int p=1;
	for(int i=1;i<=n;i++)
	{
		int c=s[i]-'a';
		if(!trie[p][c]) trie[p][c]=++tot;
		p=trie[p][c];
	}
}
queue<int> q;
int id[N];
void copy(int a,int b)
{
	fa[a]=fa[b];
	len[a]=len[b];
	for(int i=0;i<26;i++)
	tr[a][i]=tr[b][i];	
}
int Extend(int last,int c)
{
	int p=last,np=++tot;
	len[np]=len[p]+1;
	while(p&&!tr[p][c])
	{
		tr[p][c]=np;
		p=fa[p];
	}	
	if(!p) fa[np]=1;
	else 
	{
		int q=tr[p][c];
		if(len[q]==len[p]+1) fa[np]=q;
		else
		{
			int nq=++tot;
			copy(nq,q);
			len[nq]=len[p]+1;
			fa[np]=fa[q]=nq;
			while(p&&tr[p][c]==q)
			{
				tr[p][c]=nq;
				p=fa[p];
			}
		}
	}
	return np;
}
void Build()
{
	tot=1;
	id[1]=1;
	q.push(1);
	while(!q.empty())
	{
		int x=q.front();
		q.pop();
		for(int c=0;c<26;c++)
		{
			int y=trie[x][c];
			if(!y) continue;
			id[y]=Extend(id[x],c);
			q.push(y);
		}
	}
}
LL cnt[N];
int k;
int vis[N];
struct edge
{
	int y,next;
}e[N];
int link[N],t=0;
void add(int x,int y)
{
	e[++t].y=y;
	e[t].next=link[x];
	link[x]=t;
}
void dfs(int x)
{
	for(int i=link[x];i;i=e[i].next)
	{
		int y=e[i].y;
		cnt[y]+=cnt[x];
		dfs(y);
	}
}
int main()
{
	freopen("string.in","r",stdin);
	freopen("string.out","w",stdout);
	scanf("%d %d",&m,&k);
	for(int x=1;x<=m;x++)
	{
		scanf("%s",s+1);
		n=strlen(s+1);
		for(int i=1;i<=n;i++)
		str[x].push_back(s[i]); 
		Insert();
	}
	Build();
	for(int i=2;i<=tot;i++)
	add(fa[i],i);
	for(int x=1;x<=m;x++)
	{
		int len=str[x].size();
		vis[1]=x;
		int p=1;
		for(int i=0;i<len;i++)
		{
			int c=str[x][i]-'a';
			p=tr[p][c];
			int q=p;
			while(q&&vis[q]!=x)
			{
				vis[q]=x;
				cnt[q]++;
				q=fa[q];
			}
		}
	}
	for(int i=2;i<=tot;i++)
	cnt[i]=(cnt[i]>=k)*(len[i]-len[fa[i]]);
	dfs(1);
	for(int x=1;x<=m;x++)
	{
		int len=str[x].size();
		LL ans=0;
		int p=1;
		for(int i=0;i<len;i++)
		{
			int c=str[x][i]-'a';
			p=tr[p][c];
			ans=ans+cnt[p];
		}
		printf("%lld ",ans);
	}
	return 0;
} 

Example 2:[ZJOI2015]诸神眷顾的幻想乡

题目中一个很重要的条件是叶子个数很小,这启示问从这里入手
我们发现每一条路径都可以在以某个叶子为根时,变成一段连向根的路径的子串,所以我们以每个叶节点为根建广义SAM,换一个新的叶节点时,在上一个基础上继续建,最后的SAM中本质不同的子串数量即为答案

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 4e6+7;
int n,r;
int tot=1;
int fa[N],tr[N][12],len[N];
void copy(int a,int b)
{
	fa[a]=fa[b];
	len[a]=len[b];
	for(int c=0;c<r;c++)
	tr[a][c]=tr[b][c];
}
int Extend(int last,int c)
{
	int p=last,np=++tot;
	len[np]=len[p]+1;
	while(p&&!tr[p][c])
	{
		tr[p][c]=np;
		p=fa[p];
	}
	if(!p) fa[np]=1;
	else 
	{
		int q=tr[p][c];
		if(len[q]==len[p]+1) fa[np]=q;
		else
		{
			int nq=++tot;
			copy(nq,q);
			len[nq]=len[p]+1;
			fa[np]=fa[q]=nq;
			while(p&&tr[p][c]==q)
			{
				tr[p][c]=nq;
				p=fa[p];
			}	
		}	
	} 
	return np;
}
struct node
{
	int y,next;
}e[2*N];
int link[N],t=0;
int deg[N];
void add(int x,int y)
{
	e[++t].y=y;
	e[t].next=link[x];
	link[x]=t;
	deg[y]++;
}
int id[N];
int col[N];
queue<int> q;
bool vis[N];
void Build(int rot)
{
	id[rot]=Extend(1,col[rot]);
	for(int i=1;i<=n;i++)
	vis[i]=0;
	q.push(rot);
	vis[rot]=1;
	while(!q.empty())
	{
		int x=q.front();
		q.pop();
		for(int i=link[x];i;i=e[i].next)
		{
			int y=e[i].y;
			if(vis[y]) continue;
			id[y]=Extend(id[x],col[y]);
			vis[y]=1;
			q.push(y);
		}
	}
}
int main()
{
	freopen("village.in","r",stdin);
	freopen("village.out","w",stdout);
	scanf("%d %d",&n,&r);
	for(int i=1;i<=n;i++)
	scanf("%d",&col[i]);
	for(int i=1;i<n;i++)
	{
		int x,y;
		scanf("%d %d",&x,&y);
		add(x,y);
		add(y,x);
	}
	for(int i=1;i<=n;i++)
	if(deg[i]==1) Build(i);
	LL ans=0;
	for(int i=1;i<=tot;i++)
	ans=ans+(len[i]-len[fa[i]]);
	cout<<ans;
	return 0;
}

常见技巧之线段树合并

我们知道每个点的Endpos是由他子节点的Endpos组成的,那么如果我们要求某个字串的所有出现位置,可以用线段树合并维护

Example 1:[NOI2018] 你的名字

题意:给定一个串S,之后若干此询问每次询问给出字符串T和一个区间(l,r)

输出T中有多少本质不同的字串,没有在S的这段区间内出现

先从部分分入手

l=1,r=n

我们对S建立SAM,然后把T放在S上跑,求出T的每个前缀和S的字串能匹配的最大长度,记为 m a t c h i match_i matchi

接着我们对T建立SAM,并求出T的每个字串第一次出现的位置

那么对于T的后缀自动机的每一个节点,其总共代表的字串个数为 l e n i − l e n f a i len_i-len_{fa_i} lenilenfai

但是其长度小于等于 m a t c h f s t [ i ] match_{fst[i]} matchfst[i]的字串和S有重复,所以不能加上

最终答案为 ∑ m a x ( l e n i − m a x ( l e n f a i , m a t c h f s t i ) , 0 ) \sum max(len_i-max(len_{fa_i},match_{fst_i}),0) max(lenimax(lenfai,matchfsti),0)

一般情况

和上边类似,只是将T在S上跑的时候需要用线段树判断这一段区间内有没有出现这个字串,用线段树合并维护即可,这里有两个细节

1是线段树合并时每次都要新建一个节点,因为我们可能会访问到一个节点,而合并的过程中可能有别的点的儿子指向它导致这个点的线段树被修改,所以每次新建一个版本

2时因为有了区间的限制,所以每个穿如果匹配不上不能直接跳父亲,而应该一位一位减掉,知道减到父亲节点的长度

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1e6+7;
struct edge
{
	int y,next;
}e[2*N];
int link[N],t=0;
void add(int x,int y)
{
	e[++t].y=y;
	e[t].next=link[x];
	link[x]=t;
}
int sum[N*50],lson[N*50],rson[N*50];
int cnt=0;
int rot[N];
char s[N];
int n,m;
int modify(int k,int l,int r,int x)
{
	if(!k) k=++cnt;
	sum[k]++;
	if(l==r) return k;
	int mid=(l+r)>>1;
	if(x<=mid) lson[k]=modify(lson[k],l,mid,x);
	else rson[k]=modify(rson[k],mid+1,r,x);
	return k;
}
int merge(int x,int y,int l,int r)
{
	if(!x||!y) return x+y;
	int k=++cnt;
	sum[k]=sum[x]+sum[y];
	if(l==r) return k;
	int mid=(l+r)>>1;
	lson[k]=merge(lson[x],lson[y],l,mid);
	rson[k]=merge(rson[x],rson[y],mid+1,r);
	return k;
} 
int query(int k,int l,int r,int L,int R)
{
	if(!k||L>R) return 0;
	if(L<=l&&r<=R) return sum[k];
	int mid=(l+r)>>1;
	int res=0;
	if(L<=mid) res=res+query(lson[k],l,mid,L,R);
	if(R>mid) res=res+query(rson[k],mid+1,r,L,R);
	return res;
}
struct SAM
{
	int tot,last;
	int len[N],fa[N],tr[N][26];
	int fst[N];
	SAM()
	{
		tot=last=1;
	}
	void clear()
	{
		for(int i=1;i<=tot;i++)
		{
			len[i]=fa[i]=fst[i]=0;
			for(int c=0;c<26;c++)
			tr[i][c]=0;
		}
		tot=last=1;
	}
	void copy(int a,int b)
	{
		fa[a]=fa[b];
		len[a]=len[b];
		fst[a]=fst[b];
		for(int i=0;i<26;i++)
		tr[a][i]=tr[b][i];
	}
	void Extend(int c,int id)
	{
		int p=last,np=last=++tot;
		len[np]=len[p]+1;
		fst[np]=id;
		while(p&&!tr[p][c]) 
		{
			tr[p][c]=np;
			p=fa[p];
		}
		if(!p) fa[np]=1;
		else 
		{
			int q=tr[p][c];
			if(len[q]==len[p]+1) fa[np]=q;
			else
			{
				int nq=++tot;
				copy(nq,q);
				len[nq]=len[p]+1;
				fa[np]=fa[q]=nq;
				while(p&&tr[p][c]==q) 
				{
					tr[p][c]=nq;
					p=fa[p];
				}
			}
		}
	}
}S,T;
void GetTree(int x)
{
	for(int i=link[x];i;i=e[i].next)
	{
		int y=e[i].y;
		GetTree(y);
		rot[x]=merge(rot[x],rot[y],1,n);
	} 
}
int match[N];
void Match(int l,int r)
{
	int len=strlen(s+1);
	int p=1,t=0;
	for(int i=1;i<=len;i++)
	{
		int c=s[i]-'a';
		while(1)
		{
			int q=S.tr[p][c];
			if(q&&query(rot[q],1,n,l+t,r))
			{
				p=q;
				t++;
				break;
			}
			if(!t) break;
			t--;
			if(t==S.len[S.fa[p]])
			p=S.fa[p];
		}
		match[i]=t;
	}
} 
int main()
{
	freopen("name.in","r",stdin);
	freopen("name.out","w",stdout);
	scanf("%s",s+1);
	n=strlen(s+1);
	S.clear();
	for(int i=1;i<=n;i++) 
	{
		S.Extend(s[i]-'a',i);
		int x=S.last;
		rot[x]=modify(rot[x],1,n,i);
	}
	for(int i=2;i<=S.tot;i++)
	add(S.fa[i],i);
	GetTree(1);
	int q;
	scanf("%d",&q);
	while(q--)
	{
		scanf("%s",s+1);
		m=strlen(s+1);
		T.clear();
		LL ans=0;
		int l,r;
		scanf("%d %d",&l,&r);
		for(int i=1;i<=m;i++)
		T.Extend(s[i]-'a',i);
		Match(l,r);
		for(int i=2;i<=T.tot;i++)
		ans+=max(T.len[i]-max(T.len[T.fa[i]],match[T.fst[i]]),0);
		printf("%lld\n",ans); 
	}
	return 0;
}

Example 2:[八省联考2018]制胡窜

思路极其复杂,以后可能会单独再写一篇

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int INF = 1e9+7;
const int N = 2e5+7;
int tr[2*N][12],len[2*N],fa[2*N];
int tot=1,last=1;
char s[N];
int n,q;
int pos[N];
void copy(int a,int b)
{
	fa[a]=fa[b];
	len[a]=len[b];
	for(int i=0;i<10;i++)
	tr[a][i]=tr[b][i];
}
void Extend(int c)
{
	int p=last,np=last=++tot;
	len[np]=len[p]+1;
	while(p&&!tr[p][c]) 
	{
		tr[p][c]=np;
		p=fa[p];
	}
	if(!p) fa[np]=1;
	else 
	{
		int q=tr[p][c];
		if(len[q]==len[p]+1) fa[np]=q;
		else
		{
			int nq=++tot;
			copy(nq,q);
			len[nq]=len[p]+1;
			fa[np]=fa[q]=nq;
			while(p&&tr[p][c]==q) 
			{
				tr[p][c]=nq;
				p=fa[p];
			}
		}
	}
}
int Min[N*40],Max[N*40],lson[N*40],rson[N*40];
int rot[N*40];
LL Add[40*N],Mul[40*N];
void Move(int a,int b)
{
	Max[a]=Max[b];
	Min[a]=Min[b];
	Add[a]=Add[b];
	Mul[a]=Mul[b];
}
void pushup(int k)
{
	if(lson[k]&&rson[k])
	{
		Min[k]=min(Min[lson[k]],Min[rson[k]]);
		Max[k]=max(Max[lson[k]],Max[rson[k]]);
		Mul[k]=Mul[lson[k]]+Mul[rson[k]]+1ll*Min[rson[k]]*(Min[rson[k]]-Max[lson[k]]);
		Add[k]=Add[lson[k]]+Add[rson[k]]+Min[rson[k]]-Max[lson[k]];		
	}
	else if(lson[k]) Move(k,lson[k]);
	else if(rson[k]) Move(k,rson[k]);
}
int Find_Max(int k,int l,int r,int L,int R)
{
	if(!k) return 0;
	if(L<=l&&r<=R) return Max[k];
	int mid=(l+r)>>1;
	int pos=0;
	if(L<=mid) pos=max(pos,Find_Max(lson[k],l,mid,L,R));
	if(R>mid) pos=max(pos,Find_Max(rson[k],mid+1,r,L,R));
	return pos;
}
int Find_Min(int k,int l,int r,int L,int R)
{
	if(!k) return INF;
	if(L<=l&&r<=R) return Min[k];
	int mid=(l+r)>>1;
	int pos=INF;
	if(L<=mid) pos=min(pos,Find_Min(lson[k],l,mid,L,R));
	if(R>mid) pos=min(pos,Find_Min(rson[k],mid+1,r,L,R));
	return pos;
}
int cnt=0;
int Insert(int k,int l,int r,int x)
{
	if(!k) k=++cnt;
	if(l==r)
	{
		Max[k]=Min[k]=x;
		Add[k]=Mul[k]=0;
		return k;
	} 
	int mid=(l+r)>>1;
	if(x<=mid) lson[k]=Insert(lson[k],l,mid,x);
	else rson[k]=Insert(rson[k],mid+1,r,x);
	pushup(k);
	return k;
}
int root;
void Get(int k,int l,int r,int L,int R)
{
	if(!k) return;
	if(L<=l&&r<=R)
	{
		if(Min[root]==0) Move(root,k);
		else
		{
			Mul[root]=Mul[root]+Mul[k]+1ll*Min[k]*(Min[k]-Max[root]);
			Add[root]=Add[root]+Add[k]+Min[k]-Max[root];			
			Min[root]=min(Min[root],Min[k]);
			Max[root]=max(Max[root],Max[k]);				
		}
		return;
	}
	int mid=(l+r)>>1;
	if(L<=mid) Get(lson[k],l,mid,L,R);
	if(R>mid) Get(rson[k],mid+1,r,L,R);
}
int Merge(int x,int y)
{
	if(!x||!y) return x+y;
	int k=++cnt;
	lson[k]=Merge(lson[x],lson[y]);
	rson[k]=Merge(rson[x],rson[y]);
	pushup(k);
	return k;
}
struct edge
{
	int y,next;
}e[2*N];
int link[N],t=0;
void add(int x,int y)
{
	e[++t].y=y;
	e[t].next=link[x];
	link[x]=t;
}
int f[N][20],dep[N];
void dfs(int x)
{
	f[x][0]=fa[x];
	for(int k=1;k<=18;k++)
	f[x][k]=f[f[x][k-1]][k-1];
	for(int i=link[x];i;i=e[i].next)
	{
		int y=e[i].y;
		dep[y]=dep[x]+1;
		dfs(y);
		if(x!=1)rot[x]=Merge(rot[x],rot[y]);
	}
}
void build()
{
	for(int i=1;i<=n;i++)
	{
		Extend(s[i]-'0');
		rot[last]=Insert(rot[last],1,n,i);
		pos[i]=last;
	}
	for(int i=2;i<=tot;i++)
	add(fa[i],i);
	dfs(1);
}
LL C(int n)
{
	if(n<2) return 0;
	return n*1ll*(n-1)/2;
}
int Find(int x,int K)
{
	for(int i=18;i>=0;i--)
	{
		if(len[f[x][i]]>=K)
		{
			x=f[x][i];
		}
	}
	return x;
}
LL calc(int l,int r)
{
	int len=r-l+1;
	int x=pos[r];
	x=Find(x,len);
	int L=Min[rot[x]],R=Max[rot[x]];
	if(L+len-1<R-len&&L+len-1<Find_Max(rot[x],1,n,L,R-len)) return C(n-1);
	if(R-len+1<=L)
	{
		int p=R-len+1;
		LL ans=Mul[rot[x]]-1ll*Add[rot[x]]*p+C(L-p)+(L-p)*1ll*(n-len);
		return C(n-1)-ans;	
	}
	else
	{
		root=cnt+1;
		Min[root]=0;
		Max[root]=0;
		Add[root]=0;
		Mul[root]=0;
		int p=R-len+1;
		int pm=Find_Max(rot[x],1,n,1,p);
		Get(rot[x],1,n,pm,L+len-1);
		int p1=Find_Max(rot[x],1,n,1,L+len-1);
		int p2=Find_Min(rot[x],1,n,L+len,n);
		LL ans=Mul[root]-1ll*Add[root]*p+(p2>p?(L-(p1-len+1))*1ll*(p2-p):0ll);
		return C(n-1)-ans;
	}
}
int main()
{
	freopen("cutting.in","r",stdin);
	freopen("cutting.out","w",stdout);
	scanf("%d %d",&n,&q);
	scanf("%s",s+1);
	build();
	while(q--)
	{
		int l,r;
		scanf("%d %d",&l,&r);
		printf("%lld\n",calc(l,r));
	}
	return 0;
} 
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值