后缀树和后缀数组
讲解:
https://blog.csdn.net/yxuanwkeith/article/details/50636898
上面两个参考一下
董老师的视频、《算法竞赛》的代码 hdu 1403
里面的三个数组sa[],rak[],height[],其实应用方面主要使用的是height[]数组,加上根据题目来的check函数,二分答案,得到结果
sa[] 后缀数组,sa[i]表示排序为i的后缀编号、rk[i]名次数组,表示后缀i的排名 height[i]高度数组,表示lcp(sa[i],sa[i-1])
1、利用倍增法和桶排序,计算sa数组
倍增法:串的宽度成倍增加,通过偏移量获取桶号 桶排序:同则入桶,不同则加桶。正序存放,逆序拿取
排序过程中先排第二关键字、再排第一关键字,然后就统计桶的个数,达到m=n的时候就可以停止了
2、利用sa[]、rk[]计算出height数组
rk[sa[i]]=i
计算height过程中,利用了定理height[rk[i]]>=height[rk[i-1]]-1 这个定理,可以不用暴力枚举,利用已经计算好的结果,加速新的结果计算
这样while过程中,k的加减不会超过n次,所以最多跑2n次
模板:luogu3809
#include<cstdio> #include<cmath> #include<algorithm> #include<cstring> #define seed 13131 #define maxn 2000010 using namespace std; typedef unsigned long long ull; char s[maxn]; int n,m; //n为后缀个数,m为桶的个数 int x[maxn],y[maxn],c[maxn],sa[maxn],rk[maxn],height[maxn]; // 桶数组x[i],辅助数组y[i],计数数组c[i] void get_sa(){ //求后缀数组 //按照第一个字母排序 for(int i=1;i<=n;i++) c[x[i]=s[i]]++; for(int i=1;i<=m;i++) c[i]+=c[i-1]; for(int i=n;i;i--) sa[c[x[i]]--]=i; //开始基数排序 for(int k=1;k<=n;k<<=1){ //按照第二关键字排序 memset(c,0,sizeof(c)); for(int i=1;i<=n;i++) y[i]=sa[i]; for(int i=1;i<=n;i++) c[x[y[i]+k]]++; //偏移量k for(int i=1;i<=m;i++) c[i]+=c[i-1]; for(int i=n;i;i--) sa[c[x[y[i]+k]]--]=y[i]; //按照第一关键字排序 memset(c,0,sizeof(c)); for(int i=1;i<=n;i++) y[i]=sa[i]; for(int i=1;i<=n;i++) c[x[y[i]]]++; for(int i=1;i<=m;i++) c[i]+=c[i-1]; for(int i=n;i;i--) sa[c[x[y[i]]]--]=y[i]; //把后缀放入桶数组 for(int i=1;i<=n;i++) y[i]=x[i]; int i; for(m=0,i=1;i<=n;i++){ if(y[sa[i]]==y[sa[i-1]]&&y[sa[i]+k]==y[sa[i-1]+k]) x[sa[i]]=m; else x[sa[i]]=++m; if(m==n) break; } } } void get_height(){ for(int i=1;i<=n;i++) rk[sa[i]]=i; for(int i=1,k=0;i<=n;i++){ //枚举后缀i if(rk[i]==1) continue; //第一名height为0 if(k) k--; //上一个后缀的height值减1 int j=sa[rk[i]-1]; //找出后缀i前邻后缀j while(i+k<=n&&j+k<=n&&s[i+k]==s[j+k]) k++; height[rk[i]]=k; } } int main(){ scanf("%s",s+1); n=strlen(s+1);m=122; get_sa(); get_height(); for(int i=1;i<=n;i++) printf("%d ",sa[i]); return 0; }
hdu 1403
最长公共子串,和最长重复子串类似,合并s1和s2,得到一个大字符串S,注意中间用‘$’分隔,避免产生更长的子串
首先计算height[]数组,查找最大的height[],然后判断他对应的sa[i]和sa[i-1]对应于大字符串的两端
#include<cstdio> #include<cmath> #include<algorithm> #include<cstring> #define seed 13131 #define maxn 200010 using namespace std; typedef unsigned long long ull; char s[maxn]; int sa[maxn],rk[maxn],height[maxn],t1[maxn],cnt[maxn],t2[maxn]; int n; void calc_sa(){ int m=127; int i,*x=t1,*y=t2; for(i=0;i<m;i++) cnt[i]=0; for(i=0;i<n;i++) cnt[x[i]=s[i]]++; for(i=1;i<m;i++) cnt[i]+=cnt[i-1]; for(i=n-1;i>=0;i--) sa[--cnt[x[i]]]=i; // for(int k=1;k<=n;k<<=1){ int p=0; for(i=n-k;i<n;i++) y[p++]=i; for(i=0;i<n;i++) if(sa[i]>=k) y[p++]=sa[i]-k; // for(i=0;i<m;i++) cnt[i]=0; for(i=0;i<n;i++) cnt[x[y[i]]]++; for(i=1;i<m;i++) cnt[i]+=cnt[i-1]; for(i=n-1;i>=0;i--) sa[--cnt[x[y[i]]]]=y[i]; swap(x,y); p=1;x[sa[0]]=0; for(i=1;i<n;i++){ x[sa[i]]=y[sa[i]]==y[sa[i-1]]&&y[sa[i-1]+k]==y[sa[i]+k]? p-1:p++; } if(p>=n) break; m=p; } } void get_hei(int n){ int i,j,k=0; for(i=0;i<n;i++) rk[sa[i]]=i; for(i=0;i<n;i++){ if(k) k--; int j=sa[rk[i]-1]; while(s[i+k]==s[j+k]) k++; height[rk[i]]=k; } } int main(){ int len1,ans; while(scanf("%s",s)!=EOF){ n=strlen(s); len1=n; s[n]='$'; scanf("%s",s+n+1); n=strlen(s); calc_sa(); get_hei(n); ans=0; // for(int i=0;i<n;i++){ // printf("sa[i]=%d rk[i]=%d\n",sa[i],rk[i]); // } // printf("\n"); // for(int i=0;i<n;i++) printf("%d ",height[i]); for(int i=1;i<n;i++){ if(height[i]>ans&&((sa[i-1]<len1&&sa[i]>=len1)||(sa[i-1]>=len1&&sa[i]<len1))) ans=height[i]; } printf("%d\n",ans); } return 0; }
一些例题: https://blog.csdn.net/qq_36038511/article/details/78133190
POJ 1743 Musical Theme
不可重叠最长重复子串
题意:有N(1 <= N <=20000)个音符的序列来表示一首乐曲,每个音符都是1..88范围内的整数,现在要找一个重复的主题。“主题”是整个音符序列的一个子串,它需要满足如下条件:
1.长度至少为5个音符。
2.在乐曲中重复出现。(可能经过转调,“转调”的意思是主题序列中每个音符都被加上或减去了同一个整数值)
3.重复出现的同一主题不能有公共部分。
#include<iostream> #include<cstring> #include<cmath> #include<algorithm> #include<stack> #include<cstdio> #include<queue> #include<map> #include<vector> #include<set> using namespace std; const int maxn=21010; const int INF=0x3fffffff; typedef long long LL; /* caioj1467: 后缀数组1:不可重叠最长重复子串 题意:有N(1 <= N <=20000)个音符的序列来表示一首乐曲,每个音符都是1..88范围内的整数,现在要找一个重复的主题。“主题”是整个音符序列的一个子串,它需要满足如下条件: 1.长度至少为5个音符。 2.在乐曲中重复出现。(可能经过转调,“转调”的意思是主题序列中每个音符都被加上或减去了同一个整数值) 3.重复出现的同一主题不能有公共部分。 原文链接:https://blog.csdn.net/qq_36038511/article/details/78133190 */ int a[maxn],tt[maxn]; char ss[maxn]; int rak[maxn],sa1[maxn],sa2[maxn]; int rsort[maxn]; void get_sa(int n,int m){ memcpy(rak,a,sizeof(rak)); memset(rsort,0,sizeof(rsort)); for(int i=1;i<=n;i++) rsort[rak[i]]++; for(int i=1;i<=m;i++) rsort[i]+=rsort[i-1]; for(int i=n;i>=1;i--) sa1[rsort[rak[i]]--]=i; int ln=1,p=0; while(p<n){ int k=0; for(int i=n-ln+1;i<=n;i++) sa2[++k]=i; for(int i=1;i<=n;i++) if(sa1[i]-ln>0) sa2[++k]=sa1[i]-ln; //第二关键字排序 memset(rsort,0,sizeof(rsort)); for(int i=1;i<=n;i++) rsort[rak[i]]++; for(int i=1;i<=m;i++) rsort[i]+=rsort[i-1]; for(int i=n;i>=1;i--) sa1[rsort[rak[sa2[i]]]--]=sa2[i]; for(int i=1;i<=n;i++) tt[i]=rak[i]; //tt辅助数组 p=1; rak[sa1[1]]=1; for(int i=2;i<=n;i++){ if(tt[sa1[i]]!=tt[sa1[i-1]]||tt[sa1[i]+ln]!=tt[sa1[i-1]+ln]) p++; rak[sa1[i]]=p; } m=p; ln*=2; } } //height[i]:sa[i]和sa[i-1]的最长公共前缀的长度 /* 定义h[i]=height[rank[i]], h数组有以下性质: h[i]≥h[i-1]-1 证明: (suffix:后缀) 设suffix(k)是排在suffix(i-1)前一名的后缀,则它们的最长公共前缀是h[i-1]。那么同时++,suffix(k+1)将排在suffix(i)的前面(这里一定h[i-1]>1,如果h[i-1]≤1,上面的原式显然成立)并且suffix(k+1)和suffix(i)的最长公共前缀是h[i-1]-1(因为同时往后挪了一位,故-1),所以suffix(i)和在它前一名的后缀的最长公共前缀至少是h[i-1]-1。因此得证。 实现的时候其实没有必要保存h数组,只须按照h[1],h[2],……,h[n]的顺序计算即可。 */ int height[maxn*10]; void get_he(int n){ //主要是这个问题 int j,k=0; for(int i=2;i<=n;i++){ j=sa1[rak[i]-1]; //前一位 if(k!=0) k--; //保证>0 while(a[j+k]==a[i+k]) k++; //暴力询问 height[rak[i]]=k; } } bool check(int k,int n){ //检查有没有重叠 for(int i=2;i<=n;i++){ if(height[i]>=k){ for(int j=i-1;j>=2;j--){ if(abs(sa1[i]-sa1[j])>=k) return 1; if(height[j]<k) break; } } } return false; } int main(){ int n; while(scanf("%d",&n)!=EOF){ if(n==0) break; for(int i=1;i<=n;i++) scanf("%d",&a[i]); int mmax=-9999999; for(int i=1;i<n;i++){ a[i]=a[i+1]-a[i]+88; if(mmax<a[i]) mmax=a[i]; } a[n]=0; n--; get_sa(n,mmax); get_he(n); int l=1,r=n,ans=1; while(l<=r){ //二分答案 int mid=(l+r)/2; if(check(mid,n)==true){ ans=mid; l=mid+1; } else r=mid-1; } if(ans<4) printf("0\n"); else printf("%d\n",ans+1); } return 0; }
后缀数组2:可重叠的k次最长重复子串
【问题描述】
农夫John发现他的奶牛产奶的质量一直在变动。经过细致的调查,他发现:虽然他不能预见明天 产奶的质量,但连续的若干天的质量有很多重叠。我们称之为一个“模式”。 John的牛奶按质量可以被赋予一个0到1000000之间的数。并且John记录了N(1<=N<=20000)天的 牛奶质量值。他想知道最长的出现了至少K(2<=K<=N)次的模式的长度。 比如1 2 3 2 3 2 3 1 中 2 3 2 3出现了两次。当K=2时,这个长度为4。(可重叠的k次最长重复子串)
#include<iostream> #include<cstring> #include<cmath> #include<algorithm> #include<stack> #include<cstdio> #include<queue> #include<map> #include<vector> #include<set> using namespace std; const int maxn=1100000; const int INF=0x3fffffff; typedef long long LL; /* caioj1468: 后缀数组2:可重叠的k次最长重复子串 【问题描述】 农夫John发现他的奶牛产奶的质量一直在变动。经过细致的调查,他发现:虽然他不能预见明天 产奶的质量,但连续的若干天的质量有很多重叠。我们称之为一个“模式”。 John的牛奶按质量可以被赋予一个0到1000000之间的数。并且John记录了N(1<=N<=20000)天的 牛奶质量值。他想知道最长的出现了至少K(2<=K<=N)次的模式的长度。 比 如1 2 3 2 3 2 3 1 中 2 3 2 3出现了两次。当K=2时,这个长度为4。(可重叠的k次最长重复子串) */ int tt[21000],a[21000],sa1[21000],sa2[21000],rak[21000]; int rsort[maxn],height[21000]; void get_sa(int n,int m){ memcpy(rak,a,sizeof(rak)); memset(rsort,0,sizeof(rsort)); for(int i=1;i<=n;i++) rsort[rak[i]]++; for(int i=1;i<=m;i++) rsort[i]+=rsort[i-1]; for(int i=n;i>=1;i--) sa1[rsort[rak[i]]--]=i; int ln=1,p=0; while(p<n){ int k=0; for(int i=n-ln+1;i<=n;i++) sa2[++k]=i; for(int i=1;i<=n;i++) if(sa1[i]>ln) sa2[++k]=sa1[i]-ln; memset(rsort,0,sizeof(rsort)); for(int i=1;i<=n;i++) rsort[rak[i]]++; for(int i=1;i<=m;i++) rsort[i]+=rsort[i-1]; for(int i=n;i>=1;i--) sa1[rsort[rak[sa2[i]]]--]=sa2[i]; memcpy(tt,rak,sizeof(rak)); p=1; rak[sa1[1]]=1; for(int i=2;i<=n;i++){ if(tt[sa1[i]]!=tt[sa1[i-1]]||tt[sa1[i]+ln]!=tt[sa1[i-1]+ln]) p++; rak[sa1[i]]=p; } m=p; ln*=2; } } void get_he(int n){ int j,k=0; for(int i=1;i<=n;i++){ j=sa1[rak[i]-1]; if(k) k--; while(a[i+k]==a[j+k]) k++; height[rak[i]]=k; } } int N,K; bool check(int x,int n){ //二分检查 int tt=1; for(int i=2;i<=n;i++){ if(height[i]>=x){ tt++; if(tt==K) return true; } else tt=1; } return false; } int main(){ while(scanf("%d %d",&N,&K)!=EOF){ int maxx=0; if(N==0) break; for(int i=1;i<=N;i++){ scanf("%d",&a[i]); maxx=max(maxx,a[i]); } get_sa(N,maxx); get_he(N); int l=1,r=N,ans=0; while(l<=r){ int mid=(l+r)/2; if(check(mid,N)) { ans=mid; l=mid+1; } else r=mid-1; } printf("%d\n",ans); } return 0; }
caioj1469: 后缀数组3:连续重复子串
【问题描述】
求两个字符串的最长公共子串。(长度不超过100000)
把两个字符串接在一起,然后在中间插入一个从没有出现过的字符
注意判断找到的公共子串会不会在同一个字符串内
#include<iostream> #include<cstring> #include<cmath> #include<algorithm> #include<stack> #include<cstdio> #include<queue> #include<map> #include<vector> #include<set> using namespace std; const int maxn=510000; const int INF=0x3fffffff; typedef long long LL; //求两个字符串的最长公共子串 //把两个字符串接在一起,然后在中间插入一个从没有出现过的字符 //注意判断找到的公共子串会不会在同一个字符串内 int a[maxn],rak[maxn],rsort[maxn],sa1[maxn],sa2[maxn]; char s1[210000],s2[210000]; int tt[maxn],height[maxn]; void get_sa(int n,int m){ for(int i=1;i<=n;i++) rak[i]=a[i]; memset(rsort,0,sizeof(rsort)); for(int i=1;i<=n;i++) rsort[rak[i]]++; for(int i=1;i<=m;i++) rsort[i]+=rsort[i-1]; for(int i=n;i>=1;i--) sa1[rsort[rak[i]]--]=i; int p=0,ln=1; while(p<n){ int k=0; for(int i=n-ln+1;i<=n;i++) sa2[++k]=i; for(int i=1;i<=n;i++) if(sa1[i]>ln) sa2[++k]=sa1[i]-ln; memset(rsort,0,sizeof(rsort)); for(int i=1;i<=n;i++) rsort[rak[i]]++; for(int i=1;i<=m;i++) rsort[i]+=rsort[i-1]; for(int i=n;i>=1;i--) sa1[rsort[rak[sa2[i]]]--]=sa2[i]; for(int i=1;i<=n;i++) tt[i]=rak[i]; p=1;rak[sa1[1]]=1; for(int i=2;i<=n;i++){ if(tt[sa1[i]]!=tt[sa1[i-1]]||tt[sa1[i]+ln]!=tt[sa1[i-1]+ln]) p++; rak[sa1[i]]=p; } m=p; ln*=2; } a[0]=0;sa1[0]=0; } void get_he(int n){ int k=0; for(int i=1;i<=n;i++){ int j=sa1[rak[i]-1]; if(k) k--; while(a[i+k]==a[j+k]) k++; height[rak[i]]=k; } } int main(){ int n,maxx=0; scanf("%s",s1+1); int lena=strlen(s1+1); scanf("%s",s2+1); int lenb=strlen(s2+1); for(int i=1;i<=lena;i++){ a[i]=s1[i]; if(maxx<a[i]) maxx=a[i]; } a[lena+1]='$'; n=lena+lenb+1; int pp=0; for(int i=lena+2;i<=n;i++){ a[i]=s2[++pp]; if(maxx<a[i]) maxx=a[i]; } get_sa(n,maxx); get_he(n); int ans=0; //能更大,也能保证不在同一个串里面 for(int i=2;i<=n;i++){ if(ans<height[i]&&((sa1[i]<=lena&&sa1[i-1]>lena+1)||(sa1[i]>lena+1&&sa1[i-1]<=lena))) ans=height[i]; } printf("%d\n",ans); return 0; }
caioj1470: 后缀数组4:Life Forms
【问题描述】
求n个字符串(长度1000)的最长的一个子串,满足该子串在一半以上的字符串中出现过,并输出该子串,如果有多个子串满足要求,则按字典序输出所有的子串;
把所有字符串都扔在一起用从未出现过的字符隔开,
然后判断在所有字符串中都出现过的字符串有多少种,
最后输出
#include <cstdio> #include <cstring> #include <algorithm> using namespace std; int a[1110000],kinds[1110000],Rank[1110000],Rsort[111000],sa1[1110000],sa2[1110000],tt[1110000],height[1110000]; char s[110000]; void get_sa(int n,int m) { for(int i=1;i<=n;i++) Rank[i]=a[i]; memset(Rsort,0,sizeof(Rsort)); for(int i=1;i<=n;i++) Rsort[Rank[i]]++; for(int i=1;i<=m;i++) Rsort[i]+=Rsort[i-1]; for(int i=n;i>=1;i--) sa1[Rsort[Rank[i]]--]=i; int p=0,ln=1; while(p<n) { int k=0; for(int i=n-ln+1;i<=n;i++) sa2[++k]=i; for(int i=1;i<=n;i++) if(sa1[i]-ln>0) sa2[++k]=sa1[i]-ln; memset(Rsort,0,sizeof(Rsort)); for(int i=1;i<=n;i++) Rsort[Rank[i]]++; for(int i=1;i<=m;i++) Rsort[i]+=Rsort[i-1]; for(int i=n;i>=1;i--) sa1[Rsort[Rank[sa2[i]]]--]=sa2[i]; for(int i=1;i<=n;i++) tt[i]=Rank[i]; p=1;Rank[sa1[1]]=1; for(int i=2;i<=n;i++) { if(tt[sa1[i]]!=tt[sa1[i-1]]||tt[sa1[i]+ln]!=tt[sa1[i-1]+ln]) p++; Rank[sa1[i]]=p; } m=p;ln*=2; } a[0]=0;sa1[0]=0; } void get_height(int n) { int k=0; for(int i=1;i<=n;i++) { int j=sa1[Rank[i]-1]; if(k) k--; while(a[i+k]==a[j+k]) k++; height[Rank[i]]=k; } } int stlen=0,v[210],start[1110000]; bool check(int k,int n,int nn) { int ks=0,kind=0,stl=0; for(int i=1;i<=n;i++) { if(height[i]<k) { if(ks>nn/2) { stl++; start[stl]=sa1[i-1]; } memset(v,0,sizeof(v)); ks=0; } kind=kinds[sa1[i]]; if(v[kind]==0&&kind>0) { v[kind]=1;ks++; } } if(ks>nn/2) { stlen++; start[stl]=sa1[n]; } if(stl) {stlen=stl;return true;} return false; } int main() { int n; while(scanf("%d",&n)!=EOF) { stlen=0; if(n==0) break; int maxx=500,st=0; for(int i=1;i<=n;i++) { scanf("%s",s+1); int len=strlen(s+1); for(int j=1;j<=len;j++) { a[j+st]=s[j]; kinds[j+st]=i; } a[len+st+1]=i+400; kinds[len+st+1]=0; st+=len+1; } get_sa(st,maxx); get_height(st); int l=1,r=1100,mid,len=0; while(l<=r) { mid=(l+r)/2; if(check(mid,st,n)) { len=mid;l=mid+1; } else r=mid-1; } for(int i=1;i<=stlen;i++) { for(int j=1;j<=len;j++) printf("%c",a[j+start[i]-1]); printf("\n"); } if(stlen==0) printf("?\n"); printf("\n"); } }
一些其他的应用
Q1:一个串中两个串的最大公共前缀是多少?
A1:这不就是Height吗?用rmq预处理,再O(1)查询。
Q2:一个串中可重叠的重复最长子串是多长?
A2:就是求任意两个后缀的最长公共前缀,而任意两个后缀的最长公共前缀都是Height 数组里某一段的最小值,那最长的就是Height中的最大值。
Q3:一个串中不可重叠的重复最长子串是多长?
A3:先二分答案,转化成判别式的问题比较好处理。假设当前需要判别长度为k是否符合要求,只需把排序后的后缀分成若干组,其中每组的后缀之间的Height 值都不小于k,再判断其中有没有不重复的后缀,具体就是看最大的SA值和最小的SA值相差超不超过k,有一组超过的话k就是合法答案。
A4:一个字符串不相等的子串的个数是多少?
Q4:每个子串一定是某个后缀的前缀,那么原问题等价于求所有后缀之间的不相同的前缀的个数。而且可以发现每一个后缀Suffix[SA[i]]的贡献是Len - SA[i] + 1,但是有子串算重复,重复的就是Heigh[i]个与前面相同的前缀,那么减去就可以了。最后,一个后缀Suffix[SA[i]]的贡献就是Len - SA[k] + 1 - Height[k]。
对于后缀数组更多的应用这里就不详细阐述,经过思考后每个人都会发现它的一些不同的用途,它的功能也许比你想象中的更强大!
标签:后缀,int,maxn,数组,sa,include,sa1 From: https://www.cnblogs.com/shirlybaby/p/17479976.html