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)

  1. ST:{2,1} \(sum=(2-1+1)*1\)
  2. ST:{2, 1},{4, 1} \(sum=(2-1+1)*1+(4-1+1)*1\)
  3. 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 ;
}
posted @ 2022-10-31 22:16  I_N_V  阅读(49)  评论(0编辑  收藏  举报