对于一个字符串的后缀按照字典序进行排序html
一般的求法是 \(nlogn\) 的倍增作法数组
网上的博客都很详细函数
这里只放一下板子,并说一下几种常见的题型spa
#define rg register const int maxn=1e6+5; int n,m,sa[maxn],fir[maxn],sec[maxn],tax[maxn],hei[maxn]; void Qsort(){ for(rg int i=0;i<=m;i++) tax[i]=0; for(rg int i=1;i<=n;i++) tax[fir[i]]++; for(rg int i=1;i<=m;i++) tax[i]+=tax[i-1]; for(rg int i=n;i>=1;i--) sa[tax[fir[sec[i]]]--]=sec[i]; } void getsa(){ m=10000; for(rg int i=1;i<=n;i++) fir[i]=s[i],sec[i]=i; Qsort(); for(rg int len=1,p=0;p<n;m=p,len<<=1){ p=0; for(rg int i=n-len+1;i<=n;i++) sec[++p]=i; for(rg int i=1;i<=n;i++) if(sa[i]>len) sec[++p]=sa[i]-len; Qsort(); std::swap(fir,sec); fir[sa[1]]=p=1; for(rg int i=2;i<=n;i++) fir[sa[i]]=(sec[sa[i]]==sec[sa[i-1]] && sec[sa[i]+len]==sec[sa[i-1]+len])?p:++p; } } void getheight(){ rg int j,k=0; for(rg int i=1;i<=n;i++){ if(k) k--; j=sa[fir[i]-1]; while(s[i+k]==s[j+k]) k++; hei[fir[i]]=k; } }
这种题通常给你一个长度为 \(n\) 的字符串,你能够选择将最前面的字符移到最后,求字典序最小的方案指针
解决方法就是把原数组复制一遍,而后跑一下后缀排序code
后两道 \(USACO\) 的题则须要把原串翻转接在最后,思想很巧妙htm
例题:P4051 [JSOI2007]字符加密 P1368 【模板】最小表示法 P6140 [USACO07NOV]Best Cow Line S P2870 [USACO07DEC]Best Cow Line Gblog
用总的子串的个数 \(\frac{n(n+1)}{2}\) 减去重复的子串的个数 \(\sum_{i=1}^{n}height[i]\)排序
例题:P2408 不一样子串个数 P4070 [SDOI2016]生成魔咒 SP705 SUBST1 - New Distinct Substrings SP694 DISUBSTR - Distinct Substrings
第二道题要稍稍作一下转化,把向结尾加字符转化成向前加字符
这样每次只会有一个新的后缀加入,咱们只须要用一个 \(set\) 找该后缀的前驱后继计算答案便可
对于\(height\)数组,有以下的式子
\(height[i]=LCP(sa[i−1],sa[i])\)
\(LCP(j,k)=min_{l=j+1}^kheightl\)
例题:P4248 [AHOI2013]差别 #3879. SvT
这两道题都利用了\(height\)数组第二个取 \(min\) 的性质
对于 \(height\) 数组中的每个值,记录一下它向左和向右能作的最远的贡献,能够用单调栈实现
核心代码
sta[++tp]=1; for(rg int i=2;i<=n;i++){ while(tp && heig[i]<=heig[sta[tp]]){ r[sta[tp]]=i; tp--; } l[i]=sta[tp]; sta[++tp]=i; } while(tp){ r[sta[tp--]]=n+1; } ans=1LL*(n+1)*n*(n-1)/2; for(rg int i=1;i<=n;i++){ ans-=2LL*(i-l[i])*(r[i]-i)*heig[i]; }
咱们把这些串连成一个长串,在串与串相接的地方插入一个没有出现过的特殊符号,防止出现重合的问题
而后求出整个串的 \(height\) 数组,并对于每个\(height\) 数组染色,标记它属于原来的哪个串
而后用双指针从前到后扫一遍,当记录到的不一样串的个数等于总的串的个数时取一下最大值
最后一道题还须要差分一下
例题:SP1811 LCS - Longest Common Substring SP10570 LONGCS - Longest Common Substring SP1812 LCS2 - Longest Common Substring II [SDOI2008]Sandy的卡片
完整代码
#include<cstdio> #include<cstring> #include<algorithm> #define rg register const int maxn=1e6+5; int n,m,sa[maxn],fir[maxn],sec[maxn],tax[maxn],hei[maxn],t,l[maxn],r[maxn],col[maxn],cnt[maxn],js,q[maxn],head,tail,ans; char s[maxn]; void Qsort(){ for(rg int i=0;i<=m;i++) tax[i]=0; for(rg int i=1;i<=n;i++) tax[fir[i]]++; for(rg int i=1;i<=m;i++) tax[i]+=tax[i-1]; for(rg int i=n;i>=1;i--) sa[tax[fir[sec[i]]]--]=sec[i]; } void getsa(){ memset(sa,0,sizeof(sa)); memset(fir,0,sizeof(fir)); memset(sec,0,sizeof(sec)); m=300; for(rg int i=1;i<=n;i++) fir[i]=s[i]-'0'+1,sec[i]=i; Qsort(); for(rg int len=1,p=0;p<n;m=p,len<<=1){ p=0; for(rg int i=n-len+1;i<=n;i++) sec[++p]=i; for(rg int i=1;i<=n;i++) if(sa[i]>len) sec[++p]=sa[i]-len; Qsort(); std::swap(fir,sec); fir[sa[1]]=p=1; for(rg int i=2;i<=n;i++) fir[sa[i]]=(sec[sa[i]]==sec[sa[i-1]] && sec[sa[i]+len]==sec[sa[i-1]+len])?p:++p; } } void getheight(){ memset(hei,0,sizeof(hei)); rg int j,k=0; for(rg int i=1;i<=n;i++){ if(k) k--; j=sa[fir[i]-1]; while(s[i+k]==s[j+k]) k++; hei[fir[i]]=k; } } void xg(rg int now,rg int op){ if(col[now]==0) return; if(cnt[col[now]]==0) js++; cnt[col[now]]+=op; if(cnt[col[now]]==0) js--; } int T; int main(){ scanf("%d",&T); while(T--){ memset(col,0,sizeof(col)); memset(l,0,sizeof(l)); memset(r,0,sizeof(r)); memset(cnt,0,sizeof(cnt)); ans=js=0; scanf("%d",&t); rg int len; for(rg int i=1;i<=t;i++){ l[i]=n+1; scanf("%s",s+n+1); len=strlen(s+n+1); n+=len; r[i]=n; s[++n]='A'+i; } if(t==1){ printf("0\n"); return 0; } getsa(); getheight(); for(rg int i=1;i<=t;i++){ for(rg int j=l[i];j<=r[i];j++){ col[fir[j]]=i; } } rg int nl=1; xg(1,1); for(rg int nr=2;nr<=n;nr++){ while(head<=tail && hei[nr]<=hei[q[tail]]) tail--; q[++tail]=nr; xg(nr,1); if(js==t){ while(js==t && nl<nr) xg(nl++,-1); nl--; xg(nl,1); } while(head<=tail && q[head]<=nl) head++; if(js==t){ ans=std::max(ans,hei[q[head]]); } } printf("%d\n",ans); } return 0; }
利用上一个题型的方法把不一样的串合并
利用单调队列求出每个 \(height\) 数组能贡献的最左和最右的距离
最后再容斥一下,减去两个单独子串的上述贡献
分别对应下面的两道题
咱们仍是用单调栈维护当前的 \(height\) 能向右和向左扩展的最长的长度
而后 \(dp\) 转移便可
核心代码(第二道)
sta[++tp]=1; for(rg int i=2;i<=n;i++){ while(tp && heig[i]<=heig[sta[tp]]){ r[sta[tp]]=i; tp--; } l[i]=sta[tp]; sta[++tp]=i; } while(tp){ r[sta[tp--]]=n+1; } for(rg int i=1;i<=n;i++) f[i]=1; for(rg int i=1;i<=n;i++){ f[heig[i]]=std::max(f[heig[i]],r[i]-l[i]); } for(rg int i=n;i>=1;i--){ f[i]=std::max(f[i],f[i+1]); }
例题:P2852 [USACO06DEC]Milk Patterns G SP8222 NSUBSTR - Substrings
主要考察怎么利用前缀和后缀的性质求相似于 \(AA\) 的子串的个数
考虑枚举一个 \(Len\) ,而后对于每一个点求出他是不是一个 \(2 \times Len\) 的 \(AA\) 串的开头 / 结尾。
咱们每隔 \(Len\) 放一个点,这样每个 长度为 \(2 \times Len\) 的 \(AA\) 串都至少会通过两个相邻的点。
因此再转换为每两个相邻的点会对 \(a, b\) 产生多少贡献。
先求出这对相邻点所表明的前缀的最长公共后缀 \(LCS\) 和 所表明的后缀的最长公共前缀 \(LCP\)
若是 \(LCP + LCS < Len\) 确定不合法
不然给合法的区间总体加一
参考洛谷题解
代码实现
#include<cstdio> #include<cstring> #include<algorithm> #define rg register inline int read(){ rg int x=0,fh=1; rg char ch=getchar(); while(ch<'0' || ch>'9'){ if(ch=='-') fh=-1; ch=getchar(); } while(ch>='0' && ch<='9'){ x=(x<<1)+(x<<3)+(ch^48); ch=getchar(); } return x*fh; } const int maxn=3e4+5; int n,m,sa[maxn],fir[maxn],sec[maxn],tax[maxn],hei[maxn],hei1[maxn],hei2[maxn],fir1[maxn],fir2[maxn],lg[maxn],mmin1[maxn][20],mmin2[maxn][20],t; char s[maxn]; void Qsort(){ for(rg int i=0;i<=m;i++) tax[i]=0; for(rg int i=1;i<=n;i++) tax[fir[i]]++; for(rg int i=1;i<=m;i++) tax[i]+=tax[i-1]; for(rg int i=n;i>=1;i--) sa[tax[fir[sec[i]]]--]=sec[i]; } void getsa(){ memset(sa,0,sizeof(sa)); memset(fir,0,sizeof(fir)); memset(sec,0,sizeof(sec)); m=3e4+1; for(rg int i=1;i<=n;i++) fir[i]=s[i],sec[i]=i; Qsort(); for(rg int len=1,p=0;p<n;m=p,len<<=1){ p=0; for(rg int i=n-len+1;i<=n;i++) sec[++p]=i; for(rg int i=1;i<=n;i++) if(sa[i]>len) sec[++p]=sa[i]-len; Qsort(); memcpy(sec,fir,sizeof(fir)); fir[sa[1]]=p=1; for(rg int i=2;i<=n;i++) fir[sa[i]]=(sec[sa[i]]==sec[sa[i-1]] && sec[sa[i]+len]==sec[sa[i-1]+len])?p:++p; } } void getheight(){ memset(hei,0,sizeof(hei)); rg int j,k=0; for(rg int i=1;i<=n;i++){ if(k) k--; j=sa[fir[i]-1]; while(s[i+k]==s[j+k]) k++; hei[fir[i]]=k; } } int cf1[maxn],cf2[maxn]; long long ans; int getans1(rg int l,rg int r){ rg int k=lg[r-l+1]; return std::min(mmin1[l][k],mmin1[r-(1<<k)+1][k]); } int getans2(rg int l,rg int r){ rg int k=lg[r-l+1]; return std::min(mmin2[l][k],mmin2[r-(1<<k)+1][k]); } int main(){ for(rg int i=2;i<maxn;i++) lg[i]=lg[i>>1]+1; t=read(); while(t--){ ans=0; memset(cf1,0,sizeof(cf1)); memset(cf2,0,sizeof(cf2)); scanf("%s",s+1); n=strlen(s+1); getsa(); getheight(); memcpy(hei1,hei,sizeof(hei)); memcpy(fir1,fir,sizeof(fir)); std::reverse(s+1,s+1+n); getsa(); getheight(); memcpy(hei2,hei,sizeof(hei)); memcpy(fir2,fir,sizeof(fir)); std::reverse(s+1,s+1+n); for(rg int i=1;i<=n;i++) mmin1[i][0]=hei1[i],mmin2[i][0]=hei2[i]; for(rg int j=1;j<=15;j++){ for(rg int i=1;i+(1<<j)-1<=n;i++){ mmin1[i][j]=std::min(mmin1[i][j-1],mmin1[i+(1<<(j-1))][j-1]); mmin2[i][j]=std::min(mmin2[i][j-1],mmin2[i+(1<<(j-1))][j-1]); } } rg int ac1,ac2,ac3,ac4,ac5; for(rg int len=1;len<=n;len++){ for(rg int i=len,j=i+len;j<=n;i+=len,j+=len){ ac1=fir2[n-i+1],ac2=fir2[n-j+1]; if(ac1>ac2) std::swap(ac1,ac2); ac3=getans2(ac1+1,ac2); ac1=fir1[i],ac2=fir1[j]; if(ac1>ac2) std::swap(ac1,ac2); ac4=getans1(ac1+1,ac2); ac3=std::min(ac3,len); ac4=std::min(ac4,len); if(ac3+ac4-1<len) continue; ac5=ac3+ac4-len; cf1[i-ac3+1]++; cf1[i-ac3+1+ac5]--; cf2[j+ac4-1-ac5+1]++; cf2[j+ac4]--; } } for(rg int i=1;i<=n+1;i++) cf1[i]+=cf1[i-1],cf2[i]+=cf2[i-1]; for(rg int i=1;i<n;i++){ ans+=1LL*cf2[i]*cf1[i+1]; } printf("%lld\n",ans); } return 0; }
将 \(height\) 数组从小到大排序后倒序枚举
用并查集维护联通块最大/最小值
每次把 \(height\) 数组所掌管的两个元素所在的集合合并
主席树+后缀数组+二分
利用了 \(lcp\) 这个函数是单峰的,而且峰值在本身这里