字符串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;
}

浙公网安备 33010602011771号