SA
后缀数组
后缀\(S[i]:S[i]=S[i,|S|]\)
后缀排序:将所有后缀 \(S[i]\) 看作独立的串,放在一起按照字典序进行升序排序。
后缀排名 \(rk[i]:rk[i]\) 表示后缀 \(S[i]\) 在后缀排序中的排名,即他是第几小的后缀。
后缀数组 \(sa[i]:sa[i]\) 表示排名第 \(i\) 小的后缀。
\(rk[sa[i]] = i\)
求后缀数组
LCP---最长公共前缀
前缀倍增法
将比较字典序的二分求 LCP 转化为倍增求 LCP。
首先等效的认为在字符串的末尾增添无限个空字符 \(∅\)。
定义 \(S(i, k) = S[i, i + 2^k − 1]\),即以 i 位置开头,长度为 \(2^k\) 的子串。
后缀 S[i] 与 S[j] 的字典序关系等价于 \(S(i, ∞)\) 与 \(S(j, ∞)\) 的字典序关系。
事实上,只需要将 \(S(i, ⌈log_2 n⌉),i = 1, 2, · · · , n\) 排序即可。
于是便可以倍增的进行排序,假设当前已经得到了 \(S(i, k)\) 的排序结果,即 \(rk[S(i, k)]\) 与 \(sa[S(i, k)]\),思考如何利用它们排序 \(S(i, k + 1)\)。
由于 \(S(i, k + 1)\) 是由 \(S(i, k)\) 和 \(S(i + 2^k, k)\) 前后拼接而成。
因此比较 \(S(i, k + 1)\) 与 \(S(j, k + 1)\) 字典序可以转化为先比较 \(S(i, k)\) 与 \(S(j, k)\),再比较 \(S(i + 2^k, k)\) 与 \(S(j + 2^k
, k)\)。
因此可以将 \(S(i, k + 1)\) 看作一个两位数,高位是 \(rk[S(i, k)]\),低位是\(rk[S(i + 2^k, k)]\)。
用基数排序就能\(O(n*logn)\)做到\((算法需要进行logn轮,每轮基数排序时间复杂度O(n))\)
Height数组
\(Height[i]\) 为后缀 \(i\) 与排名在他前面一个的后缀的 \(LCP\),即:\(Height[i] = LCP(S[i,n], S[sa[rk[i] − 1], n])\)。
性质
\(Height[i-1]-1\leq Height[i]\)
后缀数组性质
设有一组排序过的字符串 \(A = [A_1, A_2, · · · , A_n]\)。
如何快速的求任意 \(A_i\) 与 \(A_j\) 的 \(LCP\)?
\(LCP(A_i,A_j)=min(LCP(A_i,A_{i+1}),LCP(A_{i+1},A_{i+2},····,LCP(A_{j-1},A_j))\)
有了 Height[i] 数组之后,任意两个后缀的 LCP 就变为区间最小值查询。
模板
struct SA{
string ch;
int n,M; //M是基数排序的范围
int sa[maxn],rk[maxn],tp[maxn],tax[maxn],height[maxn];
/*
sa[i] 长度为x(任意)的后缀中,排名为i的后缀的位置
rk[i] 长度为x(任意)的后缀中,从第i位置开始的后缀的排名
tp[i] 长度为x(任意)的后缀中,第二关键字排名为i的后缀位置
*/
void Qsort(){
for(int i=0;i<=M;i++)tax[i]=0;
for(int i=1;i<=n;i++)tax[rk[i]]++;
for(int i=1;i<=M;i++)tax[i]+=tax[i-1];
for(int i=n;i>=1;i--)sa[ tax[rk[tp[i]]]-- ]=tp[i];
return ;
}
void get_sa(string s,int m){
ch=s;n=s.length();M=m;
ch="!"+ch;
for(int i=1;i<=n;i++)rk[i]=ch[i]-'a'+1,tp[i]=i; //ch[i]可能不是字母
Qsort();
for(int w=1,p=0;w<=n;w<<=1,M=p){
p=0;
for(int i=n-w+1;i<=n;i++)tp[++p]=i;
for(int i=1;i<=n;i++)if(sa[i]>w)tp[++p]=sa[i]-w;
Qsort();swap(tp,rk);rk[sa[1]]=p=1;
for(int i=2;i<=n;i++){
if(tp[sa[i-1]]==tp[sa[i]] && tp[sa[i-1]+w]==tp[sa[i]+w])rk[sa[i]]=p;
else rk[sa[i]]=++p;
}
if(p==n)break;
}
return ;
}
void get_height(){
for(int i=1;i<=n;i++)rk[sa[i]]=i;
height[0]=0;
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 && ch[i+k]==ch[j+k])k++;
height[rk[i]]=k;
}
return ;
}
ll get_sub(){ //求本质不同字串个数
ll ans=0;
for(int i=1;i<=n;i++)ans+=n-sa[i]+1-height[i];
return ans;
}
void debug(){
for(int i=1;i<=n;i++)cout<<sa[i]<<" ";puts("");
for(int i=1;i<=n;i++)cout<<height[i]<<" ";puts("");
return ;
}
}S;
例题
1.SA模板测试
2.本质不同子串个数
求本质不同子串个数=\(\sum n-sa_i+1-height_i\)
点击查看代码
#include <bits/stdc++.h>
#define ll long long
#define pa pair<int,int>
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define YES {puts("YES");return;}
#define NO {puts("NO");return ;}
using namespace std;
const int maxn=1e6+1011;
const int MOD=998244353;
const int inf=2147483647;
const double pi=acos(-1);
const double eps=1e-12;
ll read(){
ll x=0,f=1;char ch=getchar();
for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
return x*f;
}
struct SA{
string ch;
int n,M=30;
int sa[maxn],rk[maxn],tp[maxn],tax[maxn],height[maxn];
void Qsort(){
for(int i=0;i<=M;i++)tax[i]=0;
for(int i=1;i<=n;i++)tax[rk[i]]++;
for(int i=1;i<=M;i++)tax[i]+=tax[i-1];
for(int i=n;i>=1;i--)sa[ tax[rk[tp[i]]]-- ]=tp[i];
return ;
}
void get_sa(string s){
ch=s;n=s.length();
ch="!"+ch;
for(int i=1;i<=n;i++)rk[i]=ch[i]-'a'+1,tp[i]=i;
Qsort();
for(int w=1,p=0;w<=n;w<<=1,M=p){
p=0;
for(int i=n-w+1;i<=n;i++)tp[++p]=i;
for(int i=1;i<=n;i++)if(sa[i]>w)tp[++p]=sa[i]-w;
Qsort();swap(tp,rk);rk[sa[1]]=p=1;
for(int i=2;i<=n;i++){
if(tp[sa[i-1]]==tp[sa[i]] && tp[sa[i-1]+w]==tp[sa[i]+w])rk[sa[i]]=p;
else rk[sa[i]]=++p;
}
if(p==n)break;
}
return ;
}
void get_height(){
for(int i=1;i<=n;i++)rk[sa[i]]=i;
height[0]=0;
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 && ch[i+k]==ch[j+k])k++;
height[rk[i]]=k;
}
return ;
}
ll get_sub(){ //求本质不同字串个数
ll ans=0;
for(int i=1;i<=n;i++)ans+=n-sa[i]+1-height[i];
return ans;
}
}S;
void solve(){
string s;cin>>s;
S.get_sa(s);S.get_height();
cout<<S.get_sub();
return ;
}
int main(){
int t=1;
while(t--)solve();
return 0;
}
3.Substring
题意:给出一个字符串,只由 abc 三种字母构成,求有多少置换意义下本质不同子串。
如果两个串在 {a,b,c} 的某种置换作用下相等,则认为是本质相同串。
题解:
由于字符集很小,可以枚举所有的 6 种置换,于是每种本质不同子串都会出现 6 种不同的版本。
那么我们把这六种情况的字符串连接成一个字符串,算出这整个字符串的本质不同子串个数
然后子串个数/6就是答案
然而有一个例外是:如果是全 a(b/c) 串,则在 6 种置换的作用下,只会出现 3 个不同版本,这个单独考虑即可。
另外不能直接连接6种字符串,因为子串会有跨越2种以上的字符串的非法子串
小trick:我们连接6种字符串,用互不相同的连接符连接,这样就能计算非法子串的个数
最后答案就是,(拼接成的字符串的本质不同子串个数-非法子串个数+3*例外情况子串个数)/6
点击查看代码
#include <bits/stdc++.h>
#define ll long long
#define pa pair<int,int>
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define YES {puts("YES");return;}
#define NO {puts("NO");return ;}
using namespace std;
const int maxn=2e6+1011;
const int MOD=998244353;
const int inf=2147483647;
const double pi=acos(-1);
const double eps=1e-12;
ll read(){
ll x=0,f=1;char ch=getchar();
for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
return x*f;
}
struct SA{
string ch;
int n,M=50; //M是基数排序的范围
int sa[maxn],rk[maxn],tp[maxn],tax[maxn],height[maxn];
void Qsort(){
for(int i=0;i<=M;i++)tax[i]=0;
for(int i=1;i<=n;i++)tax[rk[i]]++;
for(int i=1;i<=M;i++)tax[i]+=tax[i-1];
for(int i=n;i>=1;i--)sa[ tax[rk[tp[i]]]-- ]=tp[i];
return ;
}
void get_sa(string s){
ch=s;n=s.length();
ch="!"+ch;
for(int i=1;i<=n;i++)rk[i]=ch[i]-'a'+1,tp[i]=i; //ch[i]可能不是字母
Qsort();
for(int w=1,p=0;w<=n;w<<=1,M=p){
p=0;
for(int i=n-w+1;i<=n;i++)tp[++p]=i;
for(int i=1;i<=n;i++)if(sa[i]>w)tp[++p]=sa[i]-w;
Qsort();swap(tp,rk);rk[sa[1]]=p=1;
for(int i=2;i<=n;i++){
if(tp[sa[i-1]]==tp[sa[i]] && tp[sa[i-1]+w]==tp[sa[i]+w])rk[sa[i]]=p;
else rk[sa[i]]=++p;
}
if(p==n)break;
}
return ;
}
void get_height(){
for(int i=1;i<=n;i++)rk[sa[i]]=i;
height[0]=0;
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 && ch[i+k]==ch[j+k])k++;
height[rk[i]]=k;
}
return ;
}
ll get_sub(){ //求本质不同字串个数
ll ans=0;
for(int i=1;i<=n;i++)ans+=n-sa[i]+1-height[i];
return ans;
}
void debug(){
for(int i=1;i<=n;i++)cout<<sa[i]<<" ";puts("");
for(int i=1;i<=n;i++)cout<<height[i]<<" ";puts("");
return ;
}
}S;
map<char,char>mm[7];
string solve(string s,int pos){
int lenth=s.length();
for(int i=0;i<lenth;i++)s[i]=mm[pos][s[i]];
return s;
}
int main(){
int n,cnt=0;
for(char i='a';i<='c';i++)for(char j='a';j<='c';j++)for(int k='a';k<='c';k++){
if(i==j || j==k || k==i)continue;
++cnt;mm[cnt]['a']=i;mm[cnt]['b']=j;mm[cnt]['c']=k;
}
while(scanf("%d",&n)!=EOF){
string s;cin>>s;
ll ans=0;
ll now=1;
for(int i=1;i<=n;i++){
if(s[i]==s[i-1] && i<n)now++;
else now=1;
ans=max(ans,now);//计算例外情况子串个数,全是a(b/c)的子串
}
string ss=s;
for(int i=2;i<=6;i++)ss=ss+(char)('c'+i-1)+solve(s,i);
S.get_sa(ss);S.get_height();
ans=S.get_sub()+ans*3;
now=n;
for(int i=2;i<=6;i++){ //减掉包含分隔字符的子串
ans-=(now+1)*(n+1);
now+=n+1;
}
cout<<ans/6<<endl;
}
return 0;
}
4.poj3415: Common Substrings
题意:给出两个字符串 S 和 T,求有多少个长度大于 K 的公共子串(区间)。
题解:
答案可以按照以下方式统计出来
枚举S的后缀L,枚举T的后缀R,求出L和R的LCP
若它们的LCP长度=l , 那么ans+=max(0, l-k+1)
那么问题就转化为如何快速枚举L,R并统计答案?
用特殊字符连接两个串,进行后缀排序,得到 Height 数组。
那么两个排序后的顺序后缀i,j 的 LCP 就是 min{ height[i+1],height[i+2],····,height[j]}
那么利用上面这个性质来维护单调递增的栈。
我们先考虑S对T的贡献(T对S类似),也就是在排序后的后缀中
若排名为i的后缀是T的后缀,那么统计排名在i之前的S的后缀对i后缀的答案贡献
如何用单调栈计算贡献?
单调递增栈ST维护两个值,height值,和height值的个数
同时维护sum值,表示当前单调栈产生的贡献
假设排名前三的height所对应是S字符串,三个height为2,4,3
那么单调栈和sum模拟如下(假设k=1)
- ST:{2,1} \(sum=(2-1+1)*1\)
- ST:{2, 1},{4, 1} \(sum=(2-1+1)*1+(4-1+1)*1\)
- ST:{2, 1},{3, 2} \(sum=(2-1+1)*1+(4-1+1)*1+(3-1+1)*1-(4-3)*1=(2-1+1)*1+(3-1+1)*2\)
为什么{4,1}消失了?不是{3,1}而是{3,2}
因为根据性质
两个排序后的顺序后缀i,j 的 LCP 就是 min{ height[i+1],height[i+2],····,height[j]}
那么新加入的height小于栈顶,那么栈顶的贡献就要减小
当遇到T字符串的后缀,就把sum累加到ans种,注意T字符串的后缀不加入到栈中,但会影响栈(也就是说要把栈顶跟T字符串的height比较来更新)
点击查看代码
#include<functional>
#include<algorithm>
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<complex>
#include<string>
#include<cstdio>
#include<vector>
#include<cmath>
#include<queue>
#include<deque>
#include<stack>
#include<map>
#define ll long long
#define pa pair<int,int>
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define YES {puts("YES");return;}
#define NO {puts("NO");return ;}
using namespace std;
const int maxn=3e5+1011;
const int MOD=998244353;
const int inf=2147483647;
ll read(){
ll x=0,f=1;char ch=getchar();
for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
return x*f;
}
struct SA{
string ch;
int n,M; //M是基数排序的范围
int sa[maxn],rk[maxn],tp[maxn],tax[maxn],height[maxn];
void Qsort(){
for(int i=0;i<=M;i++)tax[i]=0;
for(int i=1;i<=n;i++)tax[rk[i]]++;
for(int i=1;i<=M;i++)tax[i]+=tax[i-1];
for(int i=n;i>=1;i--)sa[ tax[rk[tp[i]]]-- ]=tp[i];
return ;
}
void get_sa(string s,int m){
ch=s;n=s.length();
ch="!"+ch;M=m;
for(int i=1;i<=n;i++)rk[i]=ch[i],tp[i]=i;
Qsort();
for(int w=1,p=0;w<=n;w<<=1,M=p){
p=0;
for(int i=n-w+1;i<=n;i++)tp[++p]=i;
for(int i=1;i<=n;i++)if(sa[i]>w)tp[++p]=sa[i]-w;
Qsort();
for(int i=1;i<=n;i++)swap(tp[i],rk[i]);
// swap(tp,rk); poj不能交换
rk[sa[1]]=p=1;
for(int i=2;i<=n;i++){
if(tp[sa[i-1]]==tp[sa[i]] && tp[sa[i-1]+w]==tp[sa[i]+w])rk[sa[i]]=p;
else rk[sa[i]]=++p;
}
if(p==n)break;
}
return ;
}
void get_height(){
for(int i=1;i<=n;i++)rk[sa[i]]=i;
height[0]=0;
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 && ch[i+k]==ch[j+k])k++;
height[rk[i]]=k;
}
return ;
}
}S;
int k;
vector<pa>st(maxn+1);
void solve(){
string s,t;cin>>s>>t;
int n=s.length();
string now=s+"#"+t;
S.get_sa(now,300);S.get_height();
ll ans=0;
//s 对 t 的贡献
int l=0;
ll sum=0;
// 从3开始,因为排名第一是#开头的后缀,排名第二的height=0
for(int i=3;i<=S.n;i++){
if(S.height[i]<k){ // height小于k,那么之前的贡献全无
l=0;sum=0;
continue;
}
int cnt=0;
if(S.sa[i-1]<=n){
cnt++;
sum+=S.height[i]-k+1;
}
while(l && st[l].fi>=S.height[i]){
sum-=(ll)(st[l].fi-S.height[i])*st[l].se;
cnt+=st[l].se;
l--;
}
st[++l]=mp(S.height[i],cnt);
if(S.sa[i]>n+1)ans+=sum;
}
//t 对 s 的贡献
l=0,sum=0;
for(int i=3;i<=S.n;i++){
if(S.height[i]<k){
l=0;sum=0;
continue;
}
int cnt=0;
if(S.sa[i-1]>n+1){
cnt++;
sum+=S.height[i]-k+1;
}
while(l && st[l].fi>=S.height[i]){
sum-=(ll)(st[l].fi-S.height[i])*st[l].se;
cnt+=st[l].se;
l--;
}
st[++l]=mp(S.height[i],cnt);
if(S.sa[i]<=n)ans+=sum;
}
cout<<ans<<endl;
return ;
}
int main(){
while((k=read())!=0)solve();
return 0;
}
5.最长公共子串
题解:
用特殊字符连接两个串,进行后缀排序,得到 H 数组。
求每一个 T 的后缀与所有的 S 后缀的最大 LCP,取最大值即为答案。
枚举每个属于 T 的后缀,向左向右寻找第一个属于 S 的后缀 \(S_l\) 和 \(S_r\),
求所有 \(max(LCP(T, S_l), LCP(T, S_r))\) 的最大值即可。
点击查看代码
void solve(){
string s,t;cin>>s>>t;
int n=s.length();
string now=s+"#"+t;
S.get_sa(now,123);S.get_height();
int ans=0,nowt=inf;
for(int i=3;i<=S.n;i++){
if(S.sa[i-1]<=n && S.sa[i]>n+1)ans=max(ans,S.height[i]);
if(S.sa[i]<=n && S.sa[i-1]>n+1)ans=max(ans,S.height[i]);
}
cout<<ans;
return ;
}
6.本质不同公共子串个数
用特殊字符连接两个串,进行后缀排序,得到 H 数组。
从左到右扫描属于 T 串的后缀,用上题的放法求每个 T 串的后缀与所有 S 串后缀的最大 LCP,记为 MXLen。
由于需要统计本质不同,需要找到前一个排名的属于 T 串的后缀,求出他们的 LCP 用于去重,记为 MNLen。
则答案 =∑max(MXLen − MNLen,0)
点击查看代码
void solve(){
string s,t;cin>>s>>t;
int n=s.length();
string now=s+"#"+t;
S.get_sa(now,123);S.get_height();
ll ans=0,tmp=0;
for(int i=3;i<=S.n;i++){
if((S.sa[i]<=n && S.sa[i-1]>n+1 ) || (S.sa[i]>n+1 && S.sa[i-1]<=n)){
ans+=max(S.height[i]-tmp,0ll);
tmp=S.height[i];
}
else tmp=min(tmp,(ll)S.height[i]);
}
cout<<ans;
return ;
}
7.瓜瓜的字符串(hard)
题意:
题解
我们先将数组翻转,那么题目就转化成从翻转数组的后缀排序后,从大到小选择后缀
点击查看代码
void solve(){
int n=read();
vector<int>now(n+2);
for(int i=1;i<=n;i++)now[i]=read();
reverse(now.begin()+1,now.end()-1);
now[n+1]=100000+10;
S.get_sa(now,100000+11);
vector<int>ans;
int pos=n+1;
for(int i=S.n-1;i>=1;i--){
if(S.sa[i]>=pos)continue;
for(int j=S.sa[i];j<pos;j++)ans.pb(S.ch[j]);
pos=min(pos,S.sa[i]);
}
for(int i=0;i<n;i++)cout<<ans[i]<<" ";
return ;
}