字符串2

后缀数组

  • \(su_i\) 表示 \(s[i,n]\)
  • \(rk_i\) 表示 \(su_i\) 在所有 \(su_j\) 中的字典序排名。
  • \(sa_i\) 表示 \(rk_i\)\(su\) 的开头位置。
  • 也就是说 \(sa_{rk_i}=rk_{sa_i}=i\)

考虑有一个字符串,我们如何求解 \(sa\) 数组。

考虑倍增,假设我们知道了所有 \(2^{w-1}\) 子串长度的排名(超出的部分算作空),即所有 \(s[i,i+2^{w-1}-1]\) 的排名 \(rk_i\),我们很容易用数对 \((rk_i,rk_{i+2^{w-1}})\) 将其拓展到 \(2^w\) 的级别。当 \(2^w>n\) 时就相当于后缀的排名了。

具体实现的化,如果真的暴力排,复杂度要到 \(O(n\log^2n)\),对于字符串一类的题目似乎复杂度不好。

模仿桶排(基排)的思想,我们的值域只有 \(n\),所以桶排能够做到 \(O(n\log n)\)。对于第二关键字的排序,如果 \(i+w-1>n\),那么肯定在最前面,否则是按已经有的 \(sa\) 来排就行。

P3809 【模板】后缀排序

#include <bits/stdc++.h>
using namespace std;
const int N=1e6+5,M=125;
string s;
int n,m,sa[N];
int rk[N],kr[N],cnt[M];
int id[N],di[N];
bool cmp(int x,int y,int k){
	return kr[x]==kr[y]&&kr[x+k]==kr[y+k];
}signed main() {
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>s,n=s.size(),s=" "+s;m=122;
	for(int i=1;i<=n;++i)++cnt[rk[i]=s[i]];
	for(int i=1;i<=m;++i)cnt[i]+=cnt[i-1];
	for(int i=n;i>=1;--i)sa[cnt[s[i]]--]=i;
	for(int k=1,p=0;k<=n;k<<=1,m=p,p=0){
		for(int i=n;i>n-k;--i)id[++p]=i;
		for(int i=1;i<=n;++i)if(sa[i]>k)id[++p]=sa[i]-k;
		for(int i=0;i<=m;++i)cnt[i]=0;
		for(int i=1;i<=n;++i)++cnt[di[i]=rk[id[i]]];
		for(int i=1;i<=m;++i)cnt[i]+=cnt[i-1];
		for(int i=n;i>=1;--i)sa[cnt[di[i]]--]=id[i];
		for(int i=1;i<=n;++i)kr[i]=rk[i];p=0;
		for(int i=1;i<=n;++i)rk[sa[i]]=cmp(sa[i-1],sa[i],k)?p:++p;
		if(p==n)break;
	}for(int i=1;i<=n;++i)
		cout<<sa[i]<<' ';
	return 0;
}

只是求解不必太在意,重点是应用。

P4248 [AHOI2013] 差异

我们在后缀数组(SA)的基础上再引出一个数组 \(ht\)

  • \(lcp(i,j)\) 定义为 \(su_i,su_j\) 的最长公共前缀。

  • \(ht_i=lcp(sa_{i-1},sa_i)\)\(ht_1=0\)

  • \(lcp(i,j)=\min_{k=\min(rk_i,rk_j)+1}^{\max(rk_i,rk_j)}ht_k\)

  • \(ht_{rk_i}\ge ht_{rk_i-1}-1\)

成分有点多,等等。

先看第三个吧,显然字典序排名差距越大,\(lcp\) 越短,然后感性理解一下,你两边的显然 \(lcp\) 显然不可能超过中间的某个位置。。。还是比较好理解的。

对于第四个,根据定义,后缀 \(i-1\) 和它前一名的后缀的 LCP 是 \(ht_{rk_{i-1}}\)
,将这个公共前缀开头扔掉一个字符的串是后缀 \(i\) 和某个串的最长公共前缀,因此 \(ht_{rk_i}\ge ht_{rk_i-1}-1\)

根据第四个性质我们可以均摊写出 \(ht\) 的代码。

	for(int i=1,k=0;i<=n;++i){
		if(k)--k;
		while(s[i+k]==s[sa[rk[i]-1]+k])++k;
		ht[rk[i]]=k;
	}

这题我们考虑 \(ht\) 数组的贡献,也就是每个 \(i\) 作为最小值覆盖的区间,使用单调栈求出区间计算即可。

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e6+5,M=125;
string s;
int n,m,sa[N];
int rk[N],kr[N],cnt[M];
int id[N],di[N],ht[N];
int st[N],l[N],r[N],top;
bool cmp(int x,int y,int k){
	return kr[x]==kr[y]&&kr[x+k]==kr[y+k];
}signed main() {
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>s,n=s.size(),s=" "+s;m=122;
	for(int i=1;i<=n;++i)++cnt[rk[i]=s[i]];
	for(int i=1;i<=m;++i)cnt[i]+=cnt[i-1];
	for(int i=n;i>=1;--i)sa[cnt[s[i]]--]=i;
	for(int k=1,p=0;k<=n;k<<=1,m=p,p=0){
		for(int i=n;i>n-k;--i)id[++p]=i;
		for(int i=1;i<=n;++i)if(sa[i]>k)id[++p]=sa[i]-k;
		for(int i=0;i<=m;++i)cnt[i]=0;
		for(int i=1;i<=n;++i)++cnt[di[i]=rk[id[i]]];
		for(int i=1;i<=m;++i)cnt[i]+=cnt[i-1];
		for(int i=n;i>=1;--i)sa[cnt[di[i]]--]=id[i];
		for(int i=1;i<=n;++i)kr[i]=rk[i];p=0;
		for(int i=1;i<=n;++i)rk[sa[i]]=cmp(sa[i-1],sa[i],k)?p:++p;
		if(p==n)break;
	}for(int i=1,k=0;i<=n;++i){
		if(k)--k;
		while(s[i+k]==s[sa[rk[i]-1]+k])++k;
		ht[rk[i]]=k;
	}st[top=1]=1;
	for(int i=2;i<=n;++i){
		while(top&&ht[i]<ht[st[top]])r[st[top--]]=i;
		l[i]=st[top],st[++top]=i;
	}while(top)r[st[top--]]=n+1;
	int ans=n*(n-1)*(n+1)/2;
	for(int i=1;i<=n;++i)
		ans-=(r[i]-i)*(i-l[i])*ht[i]*2;
	cout<<ans<<'\n';
	return 0;
}

P5028 Annihilate

考虑拼一个 \(t=s_1s_2s_3...s_n\),其中相邻的字符串之间再加一个分隔符。跑一遍 SA,求出 \(ht\),最长公共子串相当于所有后缀的 \(lcp\) 中找。

枚举后缀,然后再每个串中找最长 \(lcp\),显然我们要找尽可能近的,维护一下每个字符串 \(ht\) 的最小值。

#include <bits/stdc++.h>
using namespace std;
const int N=55,M=1e6+100;
int n,m,k,sa[M],gty[M];
int rk[M],kr[M],cnt[M];
int id[M],di[M],ht[M];
int w[N],ans[N][N];
string t[N],s="";
bool cmp(int x,int y,int k){
	return kr[x]==kr[y]&&kr[x+k]==kr[y+k];
}signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>k;
	for(int i=1,top=0;i<=k;++i){
		cin>>t[i],s+=t[i],s+=(char)(i);
		for(int j=0;j<t[i].size();++j){
			gty[++top]=i;
		}gty[++top]=i;
	}n=s.size(),s=" "+s;m=122;
	for(int i=1;i<=n;++i)++cnt[rk[i]=s[i]];
	for(int i=1;i<=m;++i)cnt[i]+=cnt[i-1];
	for(int i=n;i>=1;--i)sa[cnt[s[i]]--]=i;
	for(int k=1,p=0;k<=n;k<<=1,m=p,p=0){
		for(int i=n;i>n-k;--i)id[++p]=i;
		for(int i=1;i<=n;++i)if(sa[i]>k)id[++p]=sa[i]-k;
		for(int i=0;i<=m;++i)cnt[i]=0;
		for(int i=1;i<=n;++i)++cnt[di[i]=rk[id[i]]];
		for(int i=1;i<=m;++i)cnt[i]+=cnt[i-1];
		for(int i=n;i>=1;--i)sa[cnt[di[i]]--]=id[i];
		for(int i=1;i<=n;++i)kr[i]=rk[i];p=0;
		for(int i=1;i<=n;++i)rk[sa[i]]=cmp(sa[i-1],sa[i],k)?p:++p;
		if(p==n)break;
	}for(int i=1,k=0;i<=n;++i){
		if(k)--k;
		while(s[i+k]==s[sa[rk[i]-1]+k])++k;
		ht[rk[i]]=k;
	}memset(w,0x3f,sizeof(w));
	for(int i=2;i<=n;++i){
		for(int j=1;j<=k;++j){
			w[j]=min(w[j],ht[i]);
		}w[gty[sa[i-1]]]=ht[i];
		for(int j=1,p=gty[sa[i]];j<=k;++j){
			ans[p][j]=ans[j][p]=max(ans[j][p],w[j]);
		}
	}for(int i=1;i<=k;++i){
		for(int j=1;j<=k;++j){
			if(i!=j)cout<<ans[i][j]<<' ';
		}cout<<'\n';
	}
	return 0;
}

P2463 [SDOI2008] Sandy 的卡片

肯定还是拼起来,跑 SA,求 \(ht\)

然后考虑二分,然后看 \(ht\)\(mid\) 的关系分组,如果组内的个数达到 \(n\) 那么就是可以的。

#include <bits/stdc++.h>
using namespace std;
const int N=1005,M=1e6+100;
int n,m,k,sa[M],gty[M];
int rk[M],kr[M],cnt[M];
int id[M],di[M],ht[M];
int s[M],t[M],vis[N],tot;
bool cmp(int x,int y,int k){
	return kr[x]==kr[y]&&kr[x+k]==kr[y+k];
}bool check(int x){
	for(int i=1,j=0;i<=n;++i){
		if(ht[i]<x)
			++tot,j=0;
		if(vis[t[sa[i]]]!=tot)
			vis[t[sa[i]]]=tot,++j;
		if(j==k)return 1;
	}return 0;
}signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>k;
	for(int i=1,top=0;i<=k;++i){
		cin>>top;
		for(int j=1,x,y=0;j<=top;++j)
			cin>>x,s[++n]=x-y+2000,t[n]=i,y=x;
		s[++n]=i+10000;
	}m=100000;
	for(int i=1;i<=n;++i)++cnt[rk[i]=s[i]];
	for(int i=1;i<=m;++i)cnt[i]+=cnt[i-1];
	for(int i=n;i>=1;--i)sa[cnt[s[i]]--]=i;
	for(int k=1,p=0;k<=n;k<<=1,m=p,p=0){
		for(int i=n;i>n-k;--i)id[++p]=i;
		for(int i=1;i<=n;++i)if(sa[i]>k)id[++p]=sa[i]-k;
		for(int i=0;i<=m;++i)cnt[i]=0;
		for(int i=1;i<=n;++i)++cnt[di[i]=rk[id[i]]];
		for(int i=1;i<=m;++i)cnt[i]+=cnt[i-1];
		for(int i=n;i>=1;--i)sa[cnt[di[i]]--]=id[i];
		for(int i=1;i<=n;++i)kr[i]=rk[i];p=0;
		for(int i=1;i<=n;++i)rk[sa[i]]=cmp(sa[i-1],sa[i],k)?p:++p;
		if(p==n)break;
	}for(int i=1,k=0;i<=n;++i){
		if(k)--k;
		while(s[i+k]==s[sa[rk[i]-1]+k])++k;
		ht[rk[i]]=k;
	}int l=0,r=k;
	while(l<r){
		int mid=l+r+1>>1;
		if(check(mid))l=mid;
		else r=mid-1;
	}cout<<l+1;
	return 0;
}

P1117 [NOI2016] 优秀的拆分

\(f_{i}\) 表示以 \(i\) 结尾 \(AA\) 的个数,\(g_i\) 表示以 \(i\) 开头 \(BB\) 的个数,答案就是 \(\sum_{i=2}^{n}f_{i-1}g_i\),配合 hash 可以获得 95pts

考虑如何快速求解 \(f\),我们枚举 \(A\) 的长度 \(len\),在字符串上每 \(len\) 个点放一个关键点,那么对于一个 \(AA\) 必然经过两个关键点。

对于两个关键点,我们求出其开头后缀的 \(lcp\),一起结尾的前缀的 \(lcs\),如果 \(lcp+lcs\ge len\),那么这一片就是有贡献的。

如图,蓝色为 \(lcs\),黄色为 \(lcp\),粉色为 \(AA\)

因此我们还要差分一下,因为要区间将 \(f\gets f+1\),对于 \(lcs,lcp\) 可以用 \(SA\),但是太烦了。因为只有 \(30000\),直接 hash 只多个 \(\log\)

  • 这里复杂度使用了 \(O(\sum_{i=1}^n\frac ni)=O(n\log n)\)
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=60005,P=1e9+7,B=131;
int t,n;char s[N];
int base[N],gty[N];
int f[N],g[N];
int get(int l,int r){
	return (gty[r]-gty[l-1]*base[r-l+1]%P+P)%P;
}signed main(){
	freopen("P1117_1.in","r",stdin);
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>t,base[0]=1;
	for(int i=1;i<N;++i)
		base[i]=base[i-1]*B%P;
	while(t--){
		cin>>s+1,n=strlen(s+1);
		for(int i=1;i<=n;++i)
			gty[i]=(gty[i-1]*B+s[i]+2-'a')%P;
		for(int i=1;i*2<=n;++i){
			for(int j=i*2;j<=n;j+=i){
				int l=1,r=i,p=j-i;
				if(s[j]!=s[p])continue;
				while(l<r){
					int mid=l+r+1>>1;
					if(get(j-mid+1,j)==get(p-mid+1,p))l=mid;
					else r=mid-1;
				}int lp=j-l+1;l=1,r=i;
				while(l<r){
					int mid=l+r+1>>1;
					if(get(j,j+mid-1)==get(p,p+mid-1))l=mid;
					else r=mid-1;
				}int rp=j+l-1;
				lp=max(lp+i-1,j);
				rp=min(rp,j+i-1);
				if(lp<=rp){
					++f[lp-i*2+1],--f[rp-i*2+2];
					++g[lp],--g[rp+1];
				}
			}
		}for(int i=1;i<=n;++i)
			f[i]+=f[i-1],g[i]+=g[i-1];
		int ans=0;
		for(int i=2;i<=n;++i)
			ans+=g[i-1]*f[i];
		for(int i=0;i<=n*2;++i)
			f[i]=g[i]=gty[i]=s[i]=0;
		cout<<ans<<'\n';
	}
	return 0;
}
posted @ 2025-08-19 21:09  zzy0618  阅读(5)  评论(0)    收藏  举报