【暖*墟】#后缀数组# 后缀数组学习与练习
后缀数组的概念及模板
(1)基础概念和变量设定
n:字符串长度;m:字符种类总数(一般设置为127)。
sa[i]:排名为i的后缀的位置。
rank[i]:从第i个位置开始的后缀(后缀i)的排名。
- 其中sa和rank的关系为:rank[sa[i]]=i,sa[rank[i]]=i。
tp[i]:基数排序的第二关键字。即第二关键字排名为i的后缀的位置。
tax[i]:i号元素出现了多少次。辅助基数排序。
s:字符串,s[i]表示字符串中第i个字符。
lcp(x,y):此处指x号后缀与y号后缀的最长公共前缀长度。
height[i]:lcp(sa[i],sa[i−1])。注意是sa数组。
即:排名为i的后缀与排名为i−1的后缀的最长公共前缀。
H[i]:height[rank[i]],即i号后缀与它前一名的后缀的最长公共前缀。
- 性质:1.H[i]⩾H[i−1]−1;2.k个连续公共前缀排名接近,在sa[]中连续出现。
(2)模板代码实现
const int maxn=500019; int n,m; char s[maxn]; int rank[maxn],b[maxn],sa[maxn],tp[maxn],tax[maxn],height[maxn]; void qsort(){ //(1)基数排序 for(int i=0;i<=m;i++) tax[i]=0; for(int i=1;i<=n;i++) tax[rank[i]]++; for(int i=1;i<=m;i++) tax[i]+=tax[i-1]; for(int i=n;i>=1;i--) sa[tax[rank[tp[i]]]--]=tp[i]; } void SuffixSort(){ //(2)后缀排序 for(int i=1;i<=n;i++) rank[i]=s[i],tp[i]=i; m=519; qsort(); //第一次基数排序 for(int k=1;k<=n;k<<=1){ int p=0; for(int i=n-k+1;i<=n;i++) tp[++p]=i; //for(int i=1;i<=k;i++) tp[++p]=n-k+i; for(int i=1;i<=n;i++) if(sa[i]>k) tp[++p]=sa[i]-k; qsort(); // swap(rank,tp); for(int i=1;i<=n;i++) //注意此处数组交换的方式 b[i]=tp[i],tp[i]=rank[i],rank[i]=b[i]; rank[sa[1]]=p=1; for(int i=2;i<=n;i++) rank[sa[i]]=(tp[sa[i]]==tp[sa[i-1]] &&tp[sa[i]+k]==tp[sa[i-1]+k])?p:++p; if(p>=n) break; m=p; } } void getH(){ //(3)求height[] int k=0; for(int i=1;i<=n;i++){ if(k) k--; int j=sa[rank[i]-1]; while(s[i+k]==s[j+k]) k++; height[rank[i]]=k; } } //注意:此模板字符串从1位置开始,即scanf("%s",s+1);
(3)后缀数组常见模型
1.可重叠的最长重复子串问题
方法:求所有height的最大值就是答案。
2.求不可重叠的最长重复子串问题
方法:二分length转换成判定性问题,将所有height大于等于的划分成若干个段。
每段两两之间都是lcp大于等于k的。如果段中有两个元素的sa值绝对值大于等于k就符合条件。
【关于height数组的分组】
其实是把n个后缀根据算出来的height分组。
sa数组是按后缀排名的,如果几个后缀有部分公共前缀,
那么在后缀排名上一定是(字典序)挨在一起的。
对于我们考虑的不同的公共前缀(直接枚举height[i]即可),
可以把后缀排名里面挨在一起、公共前缀长度大于二分的值的,分为一组。
再判断组中是否满足有长度大于二分值的公共字串(即后缀的公共前缀)。
3.可重叠的k次最长重复子串
方法:判断是否存在一个划分出来的段的长度大于等于 k - 1 。
此处要注意,k - 1 个 height[ ] 就是 k 个后缀串。
4.求重复出现的子串数目
方法:可以考虑,每一个height值就能贡献当前值的答案;
但是这样算下来,会有的子串算重复。
考虑当前的和排名靠前一个的:如果当前的height大于前height的值,
那么就多加上了前一个的height值,减去即可。
5.不相同子串计数问题
方法:只考虑后缀的前缀,则排第 k 名的后缀有 n−sa[k]+1 个前缀,
但其中有 height[k] 个前缀和上一个前缀相等,故有 n−sa[k]+1−height[k] 个子串。
6.字典序第k小字符串问题
方法:同上一个,我们在对不相同子串进行计数的时候,
子串的字典序值是递增的,这样算到分界点,然后进行二分或者枚举。
7.连续重复子串问题
方法:首先枚举子串的长度k,如果符合题目要求 suffix(1) 和 suffix(k + 1) 的 lcp 应当是k + 1。
这个就用些操作随便处理了,可以用st表, 也可以考虑两个后缀的sa必然相邻。
8.多个字符串的相关问题
方法:将字符串头尾相连(注意是否要顺反连两次),
中间添加一个(未出现的)极大的字符,就可以进行一些常规操作了。
后缀数组的相关练习题集
#include <cmath> #include <iostream> #include <cstdio> #include <string> #include <cstring> #include <vector> #include <algorithm> #include <stack> #include <queue> using namespace std; typedef long long ll; typedef unsigned long long ull; //【p3809】后缀数组 const int MAXN=1e6+10; char s[MAXN]; int n,m,rank[MAXN],sa[MAXN],tax[MAXN],tp[MAXN]; void Qsort(){ //[基数排序] for(int i=0;i<=m;i++) tax[i]=0; //tax数组类似桶,将桶清空 for(int i=1;i<=n;i++) tax[rank[i]]++; //元素放入桶中 for(int i=1;i<=m;i++) tax[i]+=tax[i-1]; //求前缀(大小排名) for(int i=n;i>=1;i--) sa[tax[rank[tp[i]]]--]=tp[i]; } void SuffixSort(){ //求出sa[]、rank[] for(int i=1;i<=n;i++) rank[i]=s[i]-'0'+1,tp[i]=i; Qsort(); for(int w=1,p=0;p<n;m=p,w<<=1){ //w:当前倍增的长度 //w=x表示已经求出了长度为x的后缀的排名,现在要更新长度为2x的后缀的排名 p=0; for(int i=1;i<=w;i++) tp[++p]=n-w+i; for(int i=1;i<=n;i++) if(sa[i]>w) tp[++p]=sa[i]-w; Qsort(); //已经更新了第二关键字,利用上一轮的rank更新本轮的sa std::swap(tp,rank); rank[sa[1]]=p=1; for(int i=2;i<=n;i++) //当两个后缀上一轮排名相同时本轮也相同 rank[sa[i]]=(tp[sa[i-1]]==tp[sa[i]] &&tp[sa[i-1]+w]==tp[sa[i]+w])?p:++p; } for(int i=1;i<=n;i++) printf("%d ",sa[i]); } int main(){ scanf("%s",s+1); n=strlen(s+1); m=127; SuffixSort(); }
#include <cmath> #include <iostream> #include <cstdio> #include <string> #include <cstring> #include <vector> #include <algorithm> #include <stack> #include <queue> using namespace std; typedef long long ll; typedef unsigned long long ull; /*【p4051】字符加密 */ /*【分析】把字符串接到自己后面变成长度为2∗len以后,进行后缀排序。 按题目要求输出结尾的字符。[破环为链+求sa数组] */ const int maxn=200019; int n,m; char s[maxn]; int rank[maxn],b[maxn],sa[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[rank[i]]++; for(int i=1;i<=m;i++) tax[i]+=tax[i-1]; for(int i=n;i>=1;i--) sa[tax[rank[tp[i]]]--]=tp[i]; } void SuffixSort(){ for(int i=1;i<=n;i++) rank[i]=s[i],tp[i]=i; m=519; qsort(); //第一次基数排序 for(int k=1;k<=n;k<<=1){ int p=0; for(int i=n-k+1;i<=n;i++) tp[++p]=i; //for(int i=1;i<=k;i++) tp[++p]=n-k+i; for(int i=1;i<=n;i++) if(sa[i]>k) tp[++p]=sa[i]-k; qsort(); // swap(rank,tp); for(int i=1;i<=n;i++) b[i]=tp[i],tp[i]=rank[i],rank[i]=b[i]; rank[sa[1]]=p=1; for(int i=2;i<=n;i++) rank[sa[i]]=(tp[sa[i]]==tp[sa[i-1]]&&tp[sa[i]+k]==tp[sa[i-1]+k])?p:++p; if(p>=n) break; m=p; } } int main(){ scanf("%s",s+1); n=strlen(s+1); for(int i=1;i<=n;i++) s[i+n]=s[i]; n+=n; SuffixSort(); for(int i=1;i<=n;i++) if(sa[i]<=n/2) putchar(s[sa[i]+n/2-1]); cout<<endl; }
#include <cmath> #include <iostream> #include <cstdio> #include <string> #include <cstring> #include <vector> #include <algorithm> #include <stack> #include <queue> using namespace std; typedef long long ll; typedef unsigned long long ull; /*【SP694】disubstr [不相同子串个数] 给定一个字符串,求不相同的子串的个数。*/ /*【分析】只考虑后缀的前缀,则排第 k 名的后缀有 n−sa[k]+1 个前缀, 但其中有 height[k] 个前缀和上一个前缀相等,故有 n−sa[k]+1−height[k] 个子串。 */ const int maxn=50019; int n,m; char s[maxn]; int rank[maxn],b[maxn],sa[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[rank[i]]++; for(int i=1;i<=m;i++) tax[i]+=tax[i-1]; for(int i=n;i>=1;i--) sa[tax[rank[tp[i]]]--]=tp[i]; } void SuffixSort(){ for(int i=1;i<=n;i++) rank[i]=s[i]-'A'+19,tp[i]=i; m=519; qsort(); //第一次基数排序 for(int k=1;k<=n;k<<=1){ int p=0; for(int i=n-k+1;i<=n;i++) tp[++p]=i; //for(int i=1;i<=k;i++) tp[++p]=n-k+i; for(int i=1;i<=n;i++) if(sa[i]>k) tp[++p]=sa[i]-k; qsort(); // swap(rank,tp); for(int i=1;i<=n;i++) b[i]=tp[i],tp[i]=rank[i],rank[i]=b[i]; rank[sa[1]]=p=1; for(int i=2;i<=n;i++) rank[sa[i]]=(tp[sa[i]]==tp[sa[i-1]]&&tp[sa[i]+k]==tp[sa[i-1]+k])?p:++p; if(p>=n) break; m=p; } } void getH(){ //height[] int k=0; for(int i=1;i<=n;i++){ if(k) k--; int j=sa[rank[i]-1]; while(s[i+k]==s[j+k]) k++; height[rank[i]]=k; } } int main(){ int T; scanf("%d",&T); while(T--){ scanf("%s",s+1); n=strlen(s+1); SuffixSort(); getH(); int ans=0; for(int i=1;i<=n;i++) ans+=n-sa[i]+1-height[i]; printf("%d\n",ans); } }
#include <cmath> #include <iostream> #include <cstdio> #include <string> #include <cstring> #include <vector> #include <algorithm> #include <stack> #include <queue> using namespace std; typedef long long ll; typedef unsigned long long ull; /*【p2743】乐曲主题 [不可重叠最长重复子串] 给定一个序列,求等价的最长子串,且长度大于5,不可重叠。 等价的定义:对序列整体加上某个数值后与另一个序列完全相等。 */ /*【分析】原长度为l+1的等价序列 <=> 差分后长度为l的相等序列。 即:求不可重叠的最长重复子串。二分答案k,问题转换为判定是否存在长度为k的最长子串。*/ int read(){ int x=0,f=1;char ss=getchar(); while(ss<'0'||ss>'9'){if(ss=='-')f=-1;ss=getchar();} while(ss>='0'&&ss<='9'){x=x*10+ss-'0';ss=getchar();}return x*f;} const int maxn=50019; int n,m,a[maxn],rank[maxn],sa[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[rank[i]]++; for(int i=1;i<=m;i++) tax[i]+=tax[i-1]; for(int i=n;i>=1;i--) sa[tax[rank[tp[i]]]--]=tp[i]; } void SuffixSort(){ for(int i=1;i<=n;i++) rank[i]=a[i],tp[i]=i; m=519; qsort(); //第一次基数排序 for(int k=1;k<=n;k<<=1){ int p=0; for(int i=n-k+1;i<=n;i++) tp[++p]=i; //for(int i=1;i<=k;i++) tp[++p]=n-k+i; for(int i=1;i<=n;i++) if(sa[i]>k) tp[++p]=sa[i]-k; qsort(); swap(rank,tp); rank[sa[1]]=p=1; for(int i=2;i<=n;i++) rank[sa[i]]=(tp[sa[i]]==tp[sa[i-1]]&&tp[sa[i]+k]==tp[sa[i-1]+k])?p:++p; if(p>=n) break; m=p; } } void getH(){ //height[] int k=0; for(int i=1;i<=n;i++){ if(k) k--; int j=sa[rank[i]-1]; while(a[i+k]==a[j+k]) k++; height[rank[i]]=k; } } int check(int x){ int mx=sa[1],mi=sa[1]; for(int i=2;i<=n;i++){ if(height[i]<x) mx=mi=sa[i]; else{ if(sa[i]<mi) mi=sa[i]; if(sa[i]>mx) mx=sa[i]; if(mx-mi>x) return true; } } return false; } int main(){ while(scanf("%d",&n)!=EOF){ if(n==0) break; for(int i=1;i<=n;i++) a[i]=read(); for(int i=1;i<n;i++) a[i]=a[i+1]-a[i]+90; n--; //差分数组长度少1 SuffixSort(); getH(); //后缀排序+height数组(连续后缀的最长公共前缀) int ans=0,L=0,R=n,mid; //二分答案+height分组判断 while(L<R){ mid=L+R>>1; if(check(mid)) ans=mid,L=mid+1; else R=mid; } if(ans<4) printf("0\n"); else printf("%d\n",ans+1); } }
#include <cmath> #include <iostream> #include <cstdio> #include <string> #include <cstring> #include <vector> #include <algorithm> #include <stack> #include <queue> using namespace std; typedef long long ll; typedef unsigned long long ull; /*【p2852】牛奶模式 [可重叠的k次最长重复子串] 给定一个序列,求序列中至少出现 k 次的可重叠的最长子串*/ /*【分析】二分答案长度l,对height数组进行分组。 即:将height不小于l的放到同一组中。对于不同的height判断是否有一组大小>=k。*/ /*【关于height数组的分组】其实是把n个后缀根据算出来的height分组。 sa数组是按后缀排名的,如果几个后缀有部分公共前缀,那么在后缀排名上一定是(字典序)挨在一起的。 对于不同的公共前缀,可以把后缀排名里面[挨在一起、公共前缀长度大于二分的值]的,分为一组。 这一组里满足有长度大于二分值的公共字串(即后缀的公共前缀),再检查组内是不是有超过k个后缀。*/ const int maxn=50019; int n,m,k,s[maxn]; struct node{ int d,id; }a[maxn]; bool cmp(node a,node b){ return a.d<b.d; } int rank[maxn],sa[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[rank[i]]++; for(int i=1;i<=m;i++) tax[i]+=tax[i-1]; for(int i=n;i>=1;i--) sa[tax[rank[tp[i]]]--]=tp[i]; } void SuffixSort(){ for(int i=1;i<=n;i++) rank[i]=s[i],tp[i]=i; m=519; qsort(); //第一次基数排序 for(int k=1;k<=n;k<<=1){ int p=0; for(int i=n-k+1;i<=n;i++) tp[++p]=i; //for(int i=1;i<=k;i++) tp[++p]=n-k+i; for(int i=1;i<=n;i++) if(sa[i]>k) tp[++p]=sa[i]-k; qsort(); swap(rank,tp); rank[sa[1]]=p=1; for(int i=2;i<=n;i++) rank[sa[i]]=(tp[sa[i]]==tp[sa[i-1]]&&tp[sa[i]+k]==tp[sa[i-1]+k])?p:++p; if(p>=n) break; m=p; } } void getH(){ //height[] int k=0; for(int i=1;i<=n;i++){ if(k) k--; int j=sa[rank[i]-1]; while(s[i+k]==s[j+k]) k++; height[rank[i]]=k; } } bool check(int mid){ int cnt=0; for(int i=2;i<=n;i++){ if(height[i]<mid) cnt=0; //另外的组 else if(++cnt>=k-1) return true; //k-1个height元素表示k个后缀串 } return false; } int main(){ scanf("%d%d",&n,&k); for(int i=1;i<=n;i++) scanf("%d",&a[i].d),a[i].id=i; sort(a+1,a+n+1,cmp); int cnt=0,lastt=0; //离散化 for(int i=1;i<=n;i++){ //利用排序连续性,将每个数字缩小 if(a[i].d==lastt) s[a[i].id]=cnt; else lastt=a[i].d,s[a[i].id]=++cnt; } SuffixSort(); getH(); int l=1,r=n,ans=0,mid; while(l<=r){ mid=(l+r)>>1; if(check(mid)) ans=mid,l=mid+1; else r=mid-1; } printf("%d\n",ans); return 0; //至少出现k次的可重叠的最长子串 }
#include <cmath> #include <iostream> #include <cstdio> #include <string> #include <cstring> #include <vector> #include <algorithm> #include <stack> #include <queue> using namespace std; typedef long long ll; typedef unsigned long long ull; /*【p4248】差异 求(n-1)*n*(n+1)/2−2×所有后缀的公共前缀长度lcp。*/ //【标签】后缀数组 + 数学分析 + 分情况讨论 + 单调栈维护dp //对于每一个height[i],若height[i-1]<=height[i], //那么height[i-1]能取到的值height[i]都能取到; //若height[i-1]>height[i],则对于i位置来说、LCP长度就是height[i]。 //用单调栈维护距i最近且小于等于height[i]的位置p。 //那么转移方程为:f[i]=f[p]+(i-p)*height[i],ans=∑f[i]。 const int maxn=500019; int n,m; char s[maxn]; int rank[maxn],b[maxn],sa[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[rank[i]]++; for(int i=1;i<=m;i++) tax[i]+=tax[i-1]; for(int i=n;i>=1;i--) sa[tax[rank[tp[i]]]--]=tp[i]; } void SuffixSort(){ for(int i=1;i<=n;i++) rank[i]=s[i],tp[i]=i; m=519; qsort(); //第一次基数排序 for(int k=1;k<=n;k<<=1){ int p=0; for(int i=n-k+1;i<=n;i++) tp[++p]=i; //for(int i=1;i<=k;i++) tp[++p]=n-k+i; for(int i=1;i<=n;i++) if(sa[i]>k) tp[++p]=sa[i]-k; qsort(); // swap(rank,tp); for(int i=1;i<=n;i++) b[i]=tp[i],tp[i]=rank[i],rank[i]=b[i]; rank[sa[1]]=p=1; for(int i=2;i<=n;i++) rank[sa[i]]=(tp[sa[i]]==tp[sa[i-1]] &&tp[sa[i]+k]==tp[sa[i-1]+k])?p:++p; if(p>=n) break; m=p; } } void getH(){ int k=0; //求height[] for(int i=1;i<=n;i++){ if(k) k--; int j=sa[rank[i]-1]; while(s[i+k]==s[j+k]) k++; height[rank[i]]=k; } } struct node{ int val,pos; }; stack <node> sta; ll f[maxn]; int main(){ scanf("%s",s+1),n=strlen(s+1); SuffixSort(); getH(); ll ans=0; for(int i=1;i<=n;i++){ int p=0; while(!sta.empty()&&sta.top().val>height[i]) sta.pop(); if(!sta.empty()) p=sta.top().pos; f[i]=f[p]+(i-p)*height[i],ans+=f[i]; sta.push((node){height[i],i}); } printf("%lld\n",(ll)(n-1)*n*(n+1)/2-2*ans); }
#include <cmath> #include <iostream> #include <cstdio> #include <string> #include <cstring> #include <vector> #include <algorithm> #include <stack> #include <queue> using namespace std; typedef long long ll; typedef unsigned long long ull; /*【p3181】找相同字符 求相同子串个数 求出在两个字符串中各取出一个子串使得这两个子串相同的方案数。*/ //通过一个无用字符拼接两个字符串,求height[],可以求出A、B相同子串。 //找到所有极大的公共串 const int maxn=500019; int n,m,len1,len2; char s[maxn],ss[maxn]; int rank[maxn],b[maxn],sa[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[rank[i]]++; for(int i=1;i<=m;i++) tax[i]+=tax[i-1]; for(int i=n;i>=1;i--) sa[tax[rank[tp[i]]]--]=tp[i]; } void SuffixSort(){ for(int i=1;i<=n;i++) rank[i]=s[i],tp[i]=i; m=519; qsort(); //第一次基数排序 for(int k=1;k<=n;k<<=1){ int p=0; for(int i=n-k+1;i<=n;i++) tp[++p]=i; //for(int i=1;i<=k;i++) tp[++p]=n-k+i; for(int i=1;i<=n;i++) if(sa[i]>k) tp[++p]=sa[i]-k; qsort(); // swap(rank,tp); for(int i=1;i<=n;i++) b[i]=tp[i],tp[i]=rank[i],rank[i]=b[i]; rank[sa[1]]=p=1; for(int i=2;i<=n;i++) rank[sa[i]]=(tp[sa[i]]==tp[sa[i-1]] &&tp[sa[i]+k]==tp[sa[i-1]+k])?p:++p; if(p>=n) break; m=p; } } void getH(){ int k=0; //求height[] for(int i=1;i<=n;i++){ if(k) k--; int j=sa[rank[i]-1]; while(s[i+k]==s[j+k]) k++; height[rank[i]]=k; } } ll sum[maxn],sta[maxn],now[maxn],top=0,ans=0; void work(){ //找出所有在A、B中的后缀串,分类加减 for(int i=1;i<=n;i++) sum[i]=sum[i-1]+(sa[i]<=len1); top=0,sta[0]=1; for(int i=1;i<=n;i++){ while(top>0&&height[sta[top]]>height[i]) top--; sta[++top]=i; now[top]=(ll)(sum[i-1]-sum[sta[top-1]-1])*height[i]+now[top-1]; if(sa[i]>len1+1) ans+=now[top]; } top=0; for(int i=1;i<=n;i++) sum[i]=sum[i-1]+(sa[i]>len1+1); for(int i=1;i<=n;i++){ //top=0相当于清空栈,now[0]一直=0,所以不用清零 while(top>0&&height[sta[top]]>height[i]) top--; sta[++top]=i; now[top]=(ll)(sum[i-1]-sum[sta[top-1]-1])*height[i]+now[top-1]; if(sa[i]<=len1) ans+=now[top]; } } int main(){ scanf("%s",s+1),n=strlen(s+1),len1=n,s[++n]=127; scanf("%s",ss+1),len2=strlen(ss+1); for(int i=1;i<=len2;i++) s[++n]=ss[i]; SuffixSort(); getH(); work(); cout<<ans<<endl; }
p.s.本zz忽然发现还有一个月就要省选了,而我还在...咕咕咕0v0...真可怕...
——时间划过风的轨迹,那个少年,还在等你