https://acm.hdu.edu.cn/showproblem.php?pid=6194
题意:
给出一个串,询问他有多少个子串恰好出现k次
用后缀数组的height数组做
我们枚举按照rank排序的长为k的后缀子区间[l,r]
设这段区间的最长公共前缀位lcp
那么可以得出结论:以sa[l]开始的,长度为1、2、……lcp的子串都至少出现了k次
但是题目要求恰好k次
我们记看第l个和第l-1个后缀的最长公共前缀为p1
第r个和第r+1个后缀的最长公共前缀为p2
那么可以得出结论
以sa[l]开始的,长度为1、2、……max(p1,p2) 的子串都出现了超过k次
所以lcp-max(p1,p2)可以得到以sa[l]开始的恰好出现k次的子串个数
注意几点:
1、l-1, r+1 如果越界,用0代替
2、我的写法因为height是相邻的两个串的lcp,所以k=1要 特判
3、后缀数组求height数组,在这里比较要用原串,不能用ASCII码,因为字符串结束符ASCII码是0
#include<bits/stdc++.h> using namespace std; #define N 100009 char ch[N]; int n,k,a[N],v[N],p,q,sa[2][N],rk[2][N],h[N]; multiset<int>s; multiset<int>::iterator it; void mul(int *sa,int *rk,int *SA,int *RK) { for(int i=1;i<=n;i++) v[rk[sa[i]]]=i; for(int i=n;i;i--) if(sa[i]>k) SA[v[rk[sa[i]-k]]--]=sa[i]-k; for(int i=n-k+1;i<=n;i++) SA[v[rk[i]]--]=i; for(int i=1;i<=n;i++) RK[SA[i]]=RK[SA[i-1]]+(rk[SA[i]]!=rk[SA[i-1]]||rk[SA[i]+k]!=rk[SA[i-1]+k]); } void presa() { p=0; q=1; for(int i=1;i<=127;++i) v[i]=0; for(int i=1;i<=n;++i) rk[0][i]=rk[1][i]=0; for(int i=1;i<=n;i++) v[a[i]]++; for(int i=1;i<=127;i++) v[i]+=v[i-1]; for(int i=1;i<=n;i++) sa[p][v[a[i]]--]=i; for(int i=1;i<=n;i++) rk[p][sa[p][i]]=rk[p][sa[p][i-1]]+(a[sa[p][i-1]]!=a[sa[p][i]]); for(k=1;k<n;k<<=1,swap(p,q)) mul(sa[p],rk[p],sa[q],rk[q]); for(int i=1,k=0;i<=n;i++) { int j=sa[p][rk[p][i]-1]; while(ch[i+k]==ch[j+k]) k++; h[rk[p][i]]=k;if(k) k--; } } void solve(int m) { h[n+1]=0; long long ans=0; if(m==1) { for(int i=1;i<=n;++i) ans+=(n-sa[p][i]+1)-max(h[i],h[i+1]); printf("%lld\n",ans); return; } s.clear(); for(int i=1;i<m-1;++i) s.insert(h[i+1]); int lcp,u,d,last=0; for(int i=m;i<=n;++i) { s.insert(h[i]); it=s.begin(); lcp=*it; u=last; d=h[i+1]; ans+=max(0,lcp-max(u,d)); last=h[i-m+2]; it=s.find(last); s.erase(it); } printf("%lld\n",ans); } int main() { int T,m; scanf("%d",&T); while(T--) { scanf("%d",&m); scanf("%s",ch+1); n=strlen(ch+1); for(int i=1;i<=n;++i) a[i]=ch[i]; presa(); solve(m); } }