写一发后缀数组套路题,看起来简单,写起来要人命哦~~~
总共13题。
分两天debug吧,有点累了~~~
suffix(后缀数组的应用)
-
sa[i] :排名第 i 的后缀在哪(i 从 1 开始)
-
rank[i]:后缀 i 排第几 (i 从 0 开始)
-
height[i]:排名为 i 和 i-1 的两个后缀的最长公共前缀(LCP)长度 (i 从 2 开始)
模板:加上RMQ操作
#include <stdio.h> #include <string.h> #include <math.h> #include <algorithm> using namespace std; const int maxn = 2000000+5; int wa[maxn],wb[maxn],wv[maxn],ws[maxn]; int sa[maxn]; int r[maxn]; int cmp(int *r,int a,int b,int l) { return r[a]==r[b]&&r[a+l]==r[b+l]; } void da(int *r,int *sa,int n,int m) { int i,j,p,*x=wa,*y=wb,*t; for(i=0; i<m; i++) ws[i]=0; for(i=0; i<n; i++) ws[x[i]=r[i]]++; for(i=1; i<m; i++) ws[i]+=ws[i-1]; for(i=n-1; i>=0; i--) sa[--ws[x[i]]]=i; for(j=1,p=1; p<n; j*=2,m=p) { for(p=0,i=n-j; i<n; i++) y[p++]=i; for(i=0; i<n; i++) if(sa[i]>=j) y[p++]=sa[i]-j; for(i=0; i<n; i++) wv[i]=x[y[i]]; for(i=0; i<m; i++) ws[i]=0; for(i=0; i<n; i++) ws[wv[i]]++; for(i=1; i<m; i++) ws[i]+=ws[i-1]; for(i=n-1; i>=0; i--) sa[--ws[wv[i]]]=y[i]; for(t=x,x=y,y=t,p=1,x[sa[0]]=0,i=1; i<n; i++) x[sa[i]]=cmp(y,sa[i-1],sa[i],j)?p-1:p++; } return; } int ranks[maxn],height[maxn]; void calheight(int *r,int *sa,int n) { int i,j,k=0; for(i=1; i<=n; i++) ranks[sa[i]]=i; for(i=0; i<n; height[ranks[i++]]=k) for(k?k--:0,j=sa[ranks[i]-1]; r[i+k]==r[j+k]; k++); return; } char str[maxn]; int f[maxn][20]; void init(int len) { for(int i = 1; i <= len; i++) f[i][0] = height[i]; for(int s = 1; (1<<s)<=len; s++) { int tmp = (1<<s); for(int i = 1; i+tmp-1<=len; i++) { f[i][s] = min(f[i][s-1],f[i+tmp/2][s-1]); } } } int cal(int l,int r) { int len = log2(r-l+1); int ans = min(f[l][len],f[r-(1<<len)+1][len]); return ans; } int main() { freopen("in.txt","r",stdin); scanf("%s",str); int n = strlen(str); for(int i = 0; i < n; i++) r[i] = str[i] - 'a' + 1; da(r,sa,n+1,300); calheight(r,sa,n); for(int i = 1; i <= n; i++) { printf("%d ",sa[i]); } puts(""); for(int i = 0; i < n; i++) { printf("%d ",ranks[i]); } puts(""); for(int i = 2; i <= n; i++) { printf("%d ",height[i]); } puts(""); init(n); //height 上的 RMQ(); 从 2开始; printf("%d ",cal(2,3)); return 0; }
例题一:最长公共前缀
给定一个字符串,询问某两个后缀的最长公共前缀。
分析:根据图,某两个后缀的LCP,是一个区间的RMQ;(也是定理)
#include <stdio.h> #include <string.h> #include <math.h> #include <algorithm> using namespace std; const int maxn = 2000000+5; int wa[maxn],wb[maxn],wv[maxn],ws[maxn]; int sa[maxn]; int r[maxn]; int cmp(int *r,int a,int b,int l) { return r[a]==r[b]&&r[a+l]==r[b+l]; } void da(int *r,int *sa,int n,int m) { int i,j,p,*x=wa,*y=wb,*t; for(i=0; i<m; i++) ws[i]=0; for(i=0; i<n; i++) ws[x[i]=r[i]]++; for(i=1; i<m; i++) ws[i]+=ws[i-1]; for(i=n-1; i>=0; i--) sa[--ws[x[i]]]=i; for(j=1,p=1; p<n; j*=2,m=p) { for(p=0,i=n-j; i<n; i++) y[p++]=i; for(i=0; i<n; i++) if(sa[i]>=j) y[p++]=sa[i]-j; for(i=0; i<n; i++) wv[i]=x[y[i]]; for(i=0; i<m; i++) ws[i]=0; for(i=0; i<n; i++) ws[wv[i]]++; for(i=1; i<m; i++) ws[i]+=ws[i-1]; for(i=n-1; i>=0; i--) sa[--ws[wv[i]]]=y[i]; for(t=x,x=y,y=t,p=1,x[sa[0]]=0,i=1; i<n; i++) x[sa[i]]=cmp(y,sa[i-1],sa[i],j)?p-1:p++; } return; } int ranks[maxn],height[maxn]; void calheight(int *r,int *sa,int n) { int i,j,k=0; for(i=1; i<=n; i++) ranks[sa[i]]=i; for(i=0; i<n; height[ranks[i++]]=k) for(k?k--:0,j=sa[ranks[i]-1]; r[i+k]==r[j+k]; k++); return; } char str[maxn]; int f[maxn][20]; void init(int len) { for(int i = 1; i <= len; i++) f[i][0] = height[i]; for(int s = 1; (1<<s)<=len; s++) { int tmp = (1<<s); for(int i = 1; i+tmp-1<=len; i++) { f[i][s] = min(f[i][s-1],f[i+tmp/2][s-1]); } } } int cal(int l,int r) { int len = log2(r-l+1); int ans = min(f[l][len],f[r-(1<<len)+1][len]); return ans; } int main() { freopen("in.txt","r",stdin); scanf("%s",str); int n = strlen(str); for(int i = 0; i < n; i++) r[i] = str[i] - 'a' + 1; da(r,sa,n+1,300); calheight(r,sa,n); for(int i = 1; i <= n; i++) { printf("%d ",sa[i]); } puts(""); for(int i = 0; i < n; i++) { printf("%d ",ranks[i]); } puts(""); for(int i = 2; i <= n; i++) { printf("%d ",height[i]); } puts(""); init(n); int l,r; // 询问后缀L,R的最长公共前缀,从0开始 scanf("%d%d",&l,&r); l = ranks[l]; r = ranks[r]; if(l>r) swap(l,r); l++; printf("%d ",cal(l,r)); return 0; }
例题二:可以重叠的最长重复子串
给定一个字符串,求最长的重复子串(出现了多次>1),这两个子串可以重叠。
分析:子串可以写成一个后缀,题目可以转换为求:最长的两个后缀的最长公共前缀。任意两个后缀的LCP,都是height数组里面某一段的最小值,那么这个值一定不大于height 的最大值。
int ans = -1; for(int i = 2; i <= n; i++) ans = max(ans,height[i]);
例题三:不可重叠最长重复子串(pku1743)
给定一个字符串,求最长重复子串,这两个子串不能重叠。(当然原题题意没这么简单咯~~~原题是音乐家演奏,求两组最长的旋律,要求这两组旋律一下(只可以相差一个常数))
我这里写字符串的,其实转换过来很简单的(考虑其差值,就转换过来了)
分析:遇到不重叠——二分分组。
先二分答案,把问题变成一个判定性题目,将排序排序后的后缀按照mid 分成若干组,每组的后缀自检的height>=mid,每个区间内找两个后缀满足不重叠即可。
原理:可以看出,游戏王成为最长公共前缀>=mid 的两个后缀,一定在一个分组里面。这个分组里面查不相交的两个位置。
int main() { freopen("in.txt","r",stdin); scanf("%s",str); int n = strlen(str); for(int i = 0; i < n; i++) r[i] = str[i] - 'a' + 1; da(r,sa,n+1,300); calheight(r,sa,n); int l = 1,r= n; int ans = 0; while(l<=r) { int mid = l + (r-l)/2; int L = inf,R= -inf; bool flag = false; for(int i = 2; i <= n; i++) { if(height[i]>=mid) { //按照 mid 分组 L = min(L,sa[i]); L = min(L,sa[i-1]); R = max(R,sa[i]); R = max(R,sa[i-1]); } else { if(L+mid+1<=R) { flag = true; ans = mid; } L = inf; R = -inf; } } if(L+mid+1<=R) flag = true; if(flag) l = mid+1; else r = mid - 1; } printf("%d ",ans); return 0; }
例题四:可重叠k次最长重复子串(pku3261)
给定一个字符串,求至少出现 k 次的最长重复子串,这 k 个子串可以重叠。
分析:此题可以重叠,二分答案,将后缀分组,由例题三和例题二,可以看出,只要判断是否存在一个分组使得其中元素>=k(重复k次嘛~~~)
int main() { freopen("in.txt","r",stdin); scanf("%s",str); int n = strlen(str); int k; scanf("%d",&k); for(int i = 0; i < n; i++) r[i] = str[i] - 'a' + 1; da(r,sa,n+1,300); calheight(r,sa,n); for(int i =2 ; i<= n;i++) printf("%d ",height[i]); puts(""); int l = 1,r= n; int ans = 0; while(l<=r) { int mid = l + (r-l)/2; int L = inf,R= -inf; bool flag = false; int cnt = 1; for(int i = 2; i <= n; i++) { if(height[i]>=mid) { //按照 mid 分组 cnt++; } else { if(cnt>=k) { flag = true; ans = mid; } L = inf; R = -inf; cnt = 1; } } if(flag) l = mid+1; else r = mid - 1; } printf("%d ",ans); return 0; }
例题五:子串的个数(spoj694,spoj705)
给定一个字符串,求不相同的子串个数。
这个题今年多校考过~~~~不过当时GG了。
每一个子串一定是某个后缀的前缀,那么原问题等价于求所有后缀之间的不相同的前缀的个数。
每个后缀产生n-sa[i] 个前缀,其中有height[i] 已经与前面重复~~~
int main() { freopen("in.txt","r",stdin); scanf("%s",str); int n = strlen(str); for(int i = 0; i < n; i++) r[i] = str[i] - 'a' + 1; da(r,sa,n+1,300); calheight(r,sa,n); for(int i =2 ; i<= n;i++) printf("%d ",height[i]); puts(""); int cnt = n - sa[1]; for(int i = 2; i <= n; i++) cnt = cnt + n - sa[i] - height[i]; printf("%d ",cnt); return 0; }
例题六:最长回文子串(ural11297)
给定一个字符串,求最长回文子串。
先搞一发Manacher;
#include <bits/stdc++.h> using namespace std; const int maxn = 1000008; char instr[maxn],str[maxn*2]; int rad[maxn*2]; int Manacher() { int i,j,maxx; int n = strlen(instr); memset(str,'#',sizeof(str)); for(i=0;i<n;i++) str[(i+1)<<1] = instr[i]; n = (n+1)<<1; str[n] = '$'; int maxRad; maxRad = j = maxx = 0; for(i = 0;i<n;i++) { if(i<maxx) rad[i] = min(rad[2*j-i],maxx-i); else rad[i] = 1; while(str[i-rad[i]]==str[i+rad[i]]) rad[i] ++; if(maxRad<rad[i]) maxRad = rad[i]; if(rad[i]+i>maxx) { j = i; maxx = rad[i] + i; } } return maxRad; } int main() { int t; scanf("%d",&t); while(t--) { scanf("%s",instr); printf("%d ",Manacher()-1); } return 0; }
但是,后缀数组的思路是非常重要的,重要到关于两个字符串的题目,extend_kmp很难的话,那么基本上就是后缀数组了~~~
分析:回文——将字符串倒着加到后面,枚举回文的中心点,而只要考虑回文的一侧,另一侧则在对称面,这两个后缀求LCP~~~
int main() { freopen("in.txt","r",stdin); scanf("%s",str); int n = strlen(str); int tmpn = n; str[n] = '$'; for(int i = n+1,j=0; i <= 2*n; i++,j++) str[i] = str[n-1-j]; puts(str); for(int i = 0; i < n; i++) r[i] = str[i] - 'a' + 2; r[n] = 1; for(int i = n+1; i < 2*n+1; i++) r[i] = str[i] - 'a' + 2; n = strlen(str); // // da(r,sa,n+1,300); // calheight(r,sa,n); // // for(int i =2 ; i<= n;i++) // printf("%d ",height[i]); // puts(""); da(r,sa,n,300); calheight(r,sa,n); for(int i = 1; i <= n; i++) printf("%d ",sa[i]); puts(""); init(n); int ans = 0; for(int i = 0; i < tmpn; i++) { //枚举中心 int l = i; int r = n - i - 1; l = ranks[l]; r = ranks[r]; l++; int lcp = cal(l,r); //回文是奇数 if(lcp*2-1>ans) { ans = lcp *2 -1; } l = i; if(l!=tmpn) { l++; r = n - l - 1; l = ranks[l]; r = ranks[r]; l++; lcp = cal(l,r); ans = max(ans,lcp*2); } } printf("%d ",ans); return 0; }