( 字符串专题 )【 后缀数组 】
推荐阅读:https://www.cnblogs.com/zwfymqz/p/8413523.html
倍增法代码模板:
#include <bits/stdc++.h> using namespace std; const int maxn = 1e6+10; char s[maxn]; int n,M,rak[maxn],sa[maxn],tax[maxn],tp[maxn],height[maxn]; void Qsort() // 基数排序 { for ( int i=0; i<=M; i++ ) tax[i] = 0; for ( int i=1; i<=n; i++ ) tax[rak[i]] ++; for ( int i=1; i<=M; i++ ) tax[i] += tax[i-1]; for ( int i=n; i>=1; i-- ) sa[ tax[rak[tp[i]]]-- ] = tp[i]; } void get_sa() { M = 75; // 固定值,和s[i]的范围有关, 如果s[i]最大是1e5, M 也设置成1e5 for ( int i=1; i<=n; i++ ) { rak[i] = s[i] - '0' + 1; tp[i] = i; } Qsort(); for ( int w=1,p=0; p<n; M=p,w*=2 ) { //w:当前倍增的长度,w = x表示已经求出了长度为x的后缀的排名,现在要更新长度为2x的后缀的排名 //p表示不同的后缀的个数,很显然原字符串的后缀都是不同的,因此p = N时可以退出循环 p = 0; //这里的p仅仅是一个计数器000 for ( int i=1; i<=w; i++ ) tp[++p] = n-w+i; //前w名是后缀长度不足2*w的后缀 for ( int i=1; i<=n; i++ ) if ( sa[i]>w ) tp[++p]=sa[i]-w; // 舍去长度不足的,按sa顺序存入 Qsort();//此时我们已经更新出了第二关键字,利用上一轮的rak更新本轮的sa std::swap(tp,rak); //这里原本tp已经没有用了,tp变成上一轮的rak,方便下面更新新的rak rak[sa[1]] = p = 1; for ( int i=2; i<=n; i++ ) { // 更新rak值 rak[sa[i]] = ( tp[sa[i-1]]==tp[sa[i]]&&tp[sa[i-1]+w]==tp[sa[i]+w] )?p:++p; } //这里当两个后缀上一轮排名相同时本轮也相同,两个后缀的名次一定是相同的 } } void get_hight() // height[i] = 排名为 i 的后缀与排名为 i−1 的后缀的最长公共前缀 { // height[i] = lcp( sa[i], sa[i-1] ); int j,k=0; for ( int i=1; i<=n; i++ ) { if ( k ) k--; int j = sa[rak[i]-1]; while ( s[i+k]==s[j+k] ) k++; height[rak[i]] = k; } } int main() { cin >> s+1 ; // 下标从1开始 n = strlen(s+1); get_sa(); for ( int i=1; i<=n; i++ ) { cout << sa[i] << " "; } return 0; }dc3法模板
/* 之前一直用倍增法,发现有些题目卡倍增法,而DC3却能AC,所以顺便弄了 DC3的模版,看以后会不会用到,嗯,就是酱紫 提一些注意点:1.MAXN开n的十倍大小; 2.dc3(r,sa,n+1,Max+1);r为待后缀处理的数组,sa为存储排名位置的数组,n+1和Max+1 都和倍增一样 3.calheight(r,sa,n);和倍增一样 模版测试题目是 SPOJ 694 / SPOJ DISUBSTR Distinct Substrings【后缀数组】不相同的子串的个数 做法和我之前写的那篇题解一样,大概就这些.. */ #include<cstdio> #include<algorithm> #include<queue> #include<iostream> #include<cmath> #include<cstring> using namespace std; #define F(x) ((x)/3+((x)%3==1?0:tb)) #define G(x) ((x)<tb?(x)*3+1:((x)-tb)*3+2) const int MAXN = 10000+100;//n*10 int sa[MAXN]; int rank[MAXN]; int height[MAXN]; int n; char s[MAXN]; int r[MAXN]; int wa[MAXN],wb[MAXN],wv[MAXN]; int wws[MAXN]; void sort(int *r,int *a,int *b,int n,int m) { int i; for(i=0;i<n;i++) wv[i]=r[a[i]]; for(i=0;i<m;i++) wws[i]=0; for(i=0;i<n;i++) wws[wv[i]]++; for(i=1;i<m;i++) wws[i]+=wws[i-1]; for(i=n-1;i>=0;i--) b[--wws[wv[i]]]=a[i]; return; } int c0(int *r,int a,int b) {return r[a]==r[b]&&r[a+1]==r[b+1]&&r[a+2]==r[b+2];} int c12(int k,int *r,int a,int b) {if(k==2) return r[a]<r[b]||r[a]==r[b]&&c12(1,r,a+1,b+1); else return r[a]<r[b]||r[a]==r[b]&&wv[a+1]<wv[b+1];} void dc3(int *r,int *sa,int n,int m) { int i,j,*rn=r+n,*san=sa+n,ta=0,tb=(n+1)/3,tbc=0,p; r[n]=r[n+1]=0; for(i=0;i<n;i++) if(i%3!=0) wa[tbc++]=i; sort(r+2,wa,wb,tbc,m); sort(r+1,wb,wa,tbc,m); sort(r,wa,wb,tbc,m); for(p=1,rn[F(wb[0])]=0,i=1;i<tbc;i++) rn[F(wb[i])]=c0(r,wb[i-1],wb[i])?p-1:p++; if(p<tbc) dc3(rn,san,tbc,p); else for(i=0;i<tbc;i++) san[rn[i]]=i; for(i=0;i<tbc;i++) if(san[i]<tb) wb[ta++]=san[i]*3; if(n%3==1) wb[ta++]=n-1; sort(r,wb,wa,ta,m); for(i=0;i<tbc;i++) wv[wb[i]=G(san[i])]=i; for(i=0,j=0,p=0;i<ta && j<tbc;p++) sa[p]=c12(wb[j]%3,r,wa[i],wb[j])?wa[i++]:wb[j++]; for(;i<ta;p++) sa[p]=wa[i++]; for(;j<tbc;p++) sa[p]=wb[j++]; return; } void calheight(int *r, int *sa, int n) { int i, j, k = 0; for (i = 1; i <= n; ++i) rank[sa[i]] = i; for (i = 0; i < n; height[rank[i++]] = k) for (k ? k-- : 0, j = sa[rank[i] - 1]; r[i + k] == r[j + k]; ++k); return; } int main() { int t; scanf("%d",&t); while(t--){ scanf("%s",s); int Max=-1; n=strlen(s); for(int i=0;i<n;i++){ r[i]=s[i]; if(r[i]>Max)Max=r[i]; } r[n]=0; dc3(r,sa,n+1,Max+1); calheight(r,sa,n); int sum=0; for(int i=2;i<=n;i++)sum+=height[i]; printf("%d\n",(1+n)*n/2-sum); } return 0; }
题意:有N(1 <= N <=20000)个音符的序列来表示一首乐曲,每个音符都是1..88范围内的整数,现在要找一个重复的主题。
“主题”是整个音符序列的一个子串,它需要满足如下条件: 1.长度至少为5个音符 2.在乐曲中重复出现(可能经过转调,“转调”的意思是主题序列中每个音符都被加上或减去了同一个整数值。) 3.重复出现的同一主题不能有公共部分。
有个非常巧妙的思路:
首先把问题转化成重复子串的问题:把原串每一位都与前一位相减。这样得出的新串如果有两个长度为n的子串相同,那么它们对应在原串的长度n+1的子串也就相似。大概描述一下不可重叠最长重复子串的解法:
O(logn)二分枚举子串长度,判断解是否成立 O(n)判断长度是否成立:把互相之间LCP大于等于长度的分为一组,这通过个扫一遍height即可,因为后缀是有序的,相邻的后缀间的LCP必定的极大的;接下来就找到每个组里后缀sa值最大和最小的,如果差值大于(等于)k就成立,因为这样小下标的后缀沿着LCP下去走k步才不会盖到大下标的后缀。
代码:
#include <iostream> #include <stdio.h> using namespace std; const int maxn = 1e6+10; int s[maxn]; int n,M,rak[maxn],sa[maxn],tax[maxn],tp[maxn],height[maxn]; void Qsort() // 基数排序 { for ( int i=0; i<=M; i++ ) tax[i] = 0; for ( int i=1; i<=n; i++ ) tax[rak[i]] ++; for ( int i=1; i<=M; i++ ) tax[i] += tax[i-1]; for ( int i=n; i>=1; i-- ) sa[ tax[rak[tp[i]]]-- ] = tp[i]; } void get_sa() { M = 400; // 固定值, 和下面的s[i]-‘0’+1 对应 for ( int i=1; i<=n; i++ ) { rak[i] = s[i] ; tp[i] = i; } Qsort(); for ( int w=1,p=0; p<n; M=p,w*=2 ) { //w:当前倍增的长度,w = x表示已经求出了长度为x的后缀的排名,现在要更新长度为2x的后缀的排名 //p表示不同的后缀的个数,很显然原字符串的后缀都是不同的,因此p = N时可以退出循环 p = 0; //这里的p仅仅是一个计数器000 for ( int i=1; i<=w; i++ ) tp[++p] = n-w+i; //前w名是后缀长度不足2*w的后缀 for ( int i=1; i<=n; i++ ) if ( sa[i]>w ) tp[++p]=sa[i]-w; // 舍去长度不足的,按sa顺序存入 Qsort();//此时我们已经更新出了第二关键字,利用上一轮的rak更新本轮的sa std::swap(tp,rak); //这里原本tp已经没有用了,tp变成上一轮的rak,方便下面更新新的rak rak[sa[1]] = p = 1; for ( int i=2; i<=n; i++ ) { // 更新rak值 rak[sa[i]] = ( tp[sa[i-1]]==tp[sa[i]]&&tp[sa[i-1]+w]==tp[sa[i]+w] )?p:++p; } //这里当两个后缀上一轮排名相同时本轮也相同,两个后缀的名次一定是相同的 } } void get_hight() // height[i] = 排名为 i 的后缀与排名为 i−1 的后缀的最长公共前缀 { // height[i] = lcp( sa[i], sa[i-1] ); int j,k=0; for ( int i=1; i<=n; i++ ) { if ( k ) k--; int j = sa[rak[i]-1]; while ( s[i+k]==s[j+k] ) k++; height[rak[i]] = k; } } int solve( int x ) { int maxx=-1, minn=0x3f3f3f3f; for ( int i=2; i<=n; i++ ) { if ( height[i]>=x ) { int pos1 = sa[i]; // 根据height数组定义 int pos2 = sa[i-1]; // 对应了两个后缀字串,取最值 maxx = max(maxx,max(pos1,pos2)); minn = min(minn,min(pos1,pos2)); if ( maxx-minn>=x ) { // 后缀字串的位置差了x, 所以一定不重叠 return 1; } } else { maxx=-1; minn=0x3f3f3f3f; } } return 0; } int main() { while ( ~scanf("%d",&n) && n ) { for ( int i=1; i<=n; i++ ) { scanf("%d",&s[i]); } n --; for ( int i=1; i<=n; i++ ) { s[i] = s[i]-s[i+1] + 90; // 求差值数列 } get_sa(); get_hight(); int ans = 0; int left = 4, right=20005; while ( left<=right ) { int mid = (left+right)/2; if ( solve(mid)==1 ) { ans = mid; left = mid + 1; } else { right = mid - 1; } } if ( ans<4 ) { printf("0\n"); } else printf("%d\n",ans+1); } return 0; }
题意:找出出现k次的可重叠的最长子串的长度
思路:用后缀数组求出lcp后,2分枚举L使得连续的lcp[i]>=L 的个数>=k-1;
代码:
void get_sa() { M = 100005; // M } int solve( int x ) { int cnt=1 ; for ( int i=2; i<=n; i++ ) { if ( height[i]>=x ) { cnt ++; } else { if ( cnt>=k ) return 1; cnt = 1; } } if ( cnt>=k ) return 1; return 0; } int main() { cin >>n >> k; for ( int i=1; i<=n; i++ ) { scanf("%d",&s[i]); } get_sa(); get_hight(); int left = 0, right = 20005; int ans=0; while ( left<=right ) { int mid = (left+right)/2; if ( solve(mid)==1 ) { ans = mid; left = mid+1; } else { right = mid-1; } } cout << ans << endl; return 0; }
题意:求不同子串的个数
思路:长度为n的字符串有n*(n+1)/2个子串,再减去相同的子串就行了
对于子串,它肯定是一个后缀的前缀,如果height[i]==k,说明后缀i-1和后缀i有k个子串相同,
这样减去它即可,即减去height数组的和即可
int main() { int listt; cin >> listt; while ( listt-- ) { cin >> s+1 ; // 下标从1开始 n = strlen(s+1); get_sa(); get_hight(); int ans = n*(n+1)/2; for ( int i=2; i<=n; i++ ) { ans -= height[i]; } cout << ans << endl; } return 0; }
例题4 :
