后缀数组
定义——来自百度百科
子串
一个字符串中连续的一段成为这个字符串的子串。
后缀
后缀是指从某个位置 i 开始到整个串末尾结束的一个特殊子串。字符串 r 的从 第 i 个字符开始的后缀表示为 Suffix(i) ,也就是Suffix(i)=r[i,len(r)] 。
子串的大小
大小比较:关于字符串的大小比较,是指通常所说的“字典顺序”比较,也就是对于两个字符串u、v,令i 从1 开始顺次比较u[i]和v[i],如果u[i]=v[i]则令 i 加1,否则若u[i]<v[i]则认为u<v,u[i]>v[i]则认为u>v(也就是v<u),比较结束。如果i>len(u)或者i>len(v)仍比较不出结果,那么若len(u)<len(v) 则认为u<v ,若len(u)=len(v) 则认为u=v ,若len(u)>len(v)则u>v。
从字符串的大小比较的定义来看,S 的两个开头位置不同的后缀u 和v 进行比较的结果不可能是相等,因为u=v 的必要条件len(u)=len(v)在这里不可能满足。
后缀数组
在后缀数组中,每个后缀我们都用它的起始位置来表示。sa[x]表示排名为x的后缀是哪一个(里面存的就是这个后缀的起始位置)。
名次数组
名次数组是与后缀数组对应的,rk[i]表示i这个后缀的排名。
#算法
那如何求后缀数组呢,也就是对这n(字符串长度)个后缀排序?
暴力做法就是对所有的后缀进行排序。这样复杂度就成了
求后缀数组一般使用倍增算法。
那么怎么倍增呢。
考虑我们暴力排序的时候,其实就是先比较每个后缀的第一位,但是可能会有第一位相同的,这时候再去比较第二位。依次类推。
假设我们现在已经对第一位排好序了,然后我们需要去比较第二位。好了,我们把前两位都比较完了,还是有不能区分出名次的后缀,这时候去比较第三位???不不不
这时候我们发现,我们要继续比较的第3位和第4位其实是其他的后缀的前两位,也就是说我们之前的时候已经获得他们的排名了。那么何必重新比较呢。继续利用之前比较的不是很好吗。
来张图图片还是来自百度百科,懒的画图
如图可以看到,对于每次排序的时候都有两个关键字,第一个关键字是之前排序的结果,第二个是我们在倍增之后获得的序号。在按照第二关键字排序的时候一定要在第一关键字的基础上。
这个其实很好懂,就像是平时排成绩表。先按照总分排名次,总分相同的再按照信息成绩(笑)排。这里也是一样,第二关键字的作用就是对于之前无法区分出来的继续进行区分。
#实现
其实后缀数组的原理并不是很难,难点在于代码的实现。其实可以使用快排,但是复杂度会多个log。为了保证他的高效,一般都会使用桶排。
具体实现见代码注释吧。真的挺绕挺难懂的。
#代码
/* * @Author: wxyww * @Date: 2018-12-18 11:09:32 * @Last Modified time: 2018-12-18 19:12:48 */ #include<cstdio> #include<iostream> #include<cstdlib> #include<cmath> #include<ctime> #include<cstring> #include<bitset> using namespace std; typedef long long ll; const int N = 1000000 + 100; ll read() { ll x=0,f=1;char c=getchar(); while(c<'0'||c>'9') { if(c=='-') f=-1; c=getchar(); } while(c>='0'&&c<='9') { x=x*10+c-'0'; c=getchar(); } return x*f; } int c[N],sa[N],x[N],y[N]; //c为桶,sa[i]表示排名为i的元素的位置,x为当前数组的样子,y[i]为按照第二关键字排序排名为i的数字的第一关键字的位置 int s[N]; int n,m; void work() { for(int i = 1;i <= n;++i) ++c[x[i] = s[i]];//处理出c数组,并且把字符串赋值到x上 for(int i = 2;i <= m;++i) c[i] += c[i - 1];//对c[i]做前缀和,来求出当前元素最靠后的排名,因为有重复的元素 for(int i = n;i >= 1;--i) sa[c[x[i]]--] = i;//c[x[i]]--是因为使得每个排名都有元素,就先给他们人为安排一种顺序 for(int k = 1;k <= n;k <<= 1) { int num = 0; for(int i = n - k + 1;i <= n;++i) y[++num] = i;//对于没有第二关键字的,他们的第二关键字是最靠前的 for(int i = 1;i <= n;++i) if(sa[i] > k) y[++num] = sa[i] - k;//sa[i]储存的是单独这个元素的排名,所以只看第二关键字的话,就这么排咯 for(int i = 1;i <= m;++i) c[i] = 0; for(int i = 1;i <= n;++i) ++c[x[i]];//还是处理出c数组 for(int i = 2;i <= m;++i) c[i] += c[i - 1]; //!!!结合第一关键字和第二关键字进行排名 for(int i = n; i >= 1;--i) sa[c[x[y[i]]]--] = y[i]; //这里要好好思考,y[i]表示第二关键字排名为i的元素的位置。x[y[i]]表示这个元素真正是多少,c[x[y[i]]]表示这种元素现在应该排多少,sa[c[x[y[i]]]]就表示这个排名所对应的元素 //!!! for(int i = 1; i <= n;++i) y[i] = 0; swap(x,y); //把x数组清空,因为一会要按照新的排名重新给x赋值,并且把现在x的值赋给y,因为一会还要用 x[sa[1]] = 1; num = 1; for(int i = 2;i <= n;++i) { if(y[sa[i]] == y[sa[i - 1]] && y[sa[i] + k] == y[sa[i - 1] + k]) x[sa[i]] = num; //如果通过当前关键字进行筛选之后,两个后缀还是无法区分出大小,那么就把他们赋值成一样大的,留给以后继续区分 else x[sa[i]] = ++num; } if(num == n) break; m = num; } for(int i = 1;i <= n;++i) printf("%d ",sa[i]); } int main() { // scanf("%s",s + 1); // n = strlen(s + 1); // m = 122; n = read(); for(int i = 1;i <= n;++i) s[i] = read(); m = 200; work(); return 0; }
LCP
仅仅是对后缀进行排名,就显示不出后缀数组的强大了。他的另一个非常常用的作用就是求LCP。
什么是LCP
LCP是指最长公共前缀。也就是我们要对后缀求公共前缀。
这有什么作用呢。对后缀求公共前缀,得到的就是一个字符串内的重复子串啊。这在后面的一些题中会有用处。
一些性质
这里我们用height[i]来表示排名为i的后缀与排名为i-1的后缀的LCP
为了方便,我们在借用定义一些数组,rk[i]表示i这个后缀的排名(其实就是上面的名次数组),h[i]表示height[rk[i]] (似乎就只能这么表达了)。建议把这里搞清楚。
性质一:
就是说,排名为i的后缀与排名为i-1的后缀的最长公共前缀大于等于排名为i-1的后缀与排名为i - 2的后缀 - 1.
性质二:
也就是说,排名为i的后缀与排名为k的后缀的最长公共前缀,等于从i到k中所有串的最长公共前缀中最小的那个。
利用性质一,我们可以求出来h数组和height数组,利用性质二,可以求出任意两个串的LCP。
求LCP
依据性质一,我们可以把h数组递推出来。直接看代码其实就很明了
void get_height() { for(int i = 1;i <= n;++i) rk[sa[i]] = i; int k = 0; for(int i = 1;i <= n;++i) { if(rk[i] == 1) continue; if(k) --k; int j = sa[rk[i] - 1]; while(j + k <= n && i + k <= n && a[j + k] == a[i + k]) ++k; height[rk[i]] = k; } }
几道例题
luogu3809
只需要求出来sa数组就行了。
bzoj1692
题解
bzoj1717
题解
POJ1743
求不重叠的最长重复子串。
二分一下答案,看是不是有重叠,就行了。
代码(POJ1743)
/* * @Author: wxyww * @Date: 2018-12-18 15:20:59 * @Last Modified time: 2018-12-18 21:49:26 */ #include<cstdio> #include<iostream> #include<cstdlib> #include<cmath> #include<ctime> #include<bitset> using namespace std; typedef long long ll; const int N = 20000 + 100,INF = 1e8; ll read() { ll x=0,f=1;char c=getchar(); while(c<'0'||c>'9') { if(c=='-') f=-1; c=getchar(); } while(c>='0'&&c<='9') { x=x*10+c-'0'; c=getchar(); } return x*f; } int sa[N],rk[N],a[N],c[N],x[N],y[N]; int height[N],h[N]; int n,m; void get_sa() { for(int i = 1;i <= m;++i) c[i] = 0; for(int i = 1;i <= n;++i) ++c[x[i] = a[i]]; for(int i = 2;i <= m;++i) c[i] += c[i - 1]; for(int i = n;i >= 1;--i) sa[c[x[i]]--] = i; for(int k = 1;k <= n;k <<= 1) { int num = 0; for(int i = n - k + 1;i <= n; ++i) y[++num] = i; for(int i = 1;i <= n;++i) if(sa[i] > k) y[++num] = sa[i] - k; for(int i = 2;i <= m;++i) c[i] = 0; for(int i = 1;i <= n;++i) ++c[x[i]]; for(int i = 1;i <= m;++i) c[i] += c[i - 1]; for(int i = n;i >= 1;--i) sa[c[x[y[i]]]--] = y[i]; swap(x,y); num = 0; x[sa[1]] = ++num; for(int i = 2;i <= n;++i) { if(y[sa[i]] == y[sa[i - 1]] && y[sa[i] + k] == y[sa[i - 1] + k]) x[sa[i]] = num; else x[sa[i]] = ++num; } if(num == n) break; m = num; } } void get_height() { for(int i = 1;i <= n;++i) rk[sa[i]] = i; int k = 0; for(int i = 1;i <= n;++i) { if(rk[i] == 1) continue; if(k) --k; int j = sa[rk[i] - 1]; while(j + k <= n && i + k <= n && a[j + k] == a[i + k]) ++k; height[rk[i]] = k; } } int check(int len) { int mn = INF,mx = -INF; for(int i = 1;i <= n;++i) { if(height[i] >= len) { mn = min(mn,min(sa[i],sa[i - 1])); mx = max(mx,max(sa[i],sa[i - 1])); if(mx - mn > len) return 1; } else mn = INF,mx = -INF; } return 0; } void erfen() { int ans = -1,l = 4,r = n; while(l <= r) { int mid = (l + r) >> 1; if(check(mid)) l = mid + 1,ans = mid; else r = mid - 1; } printf("%d\n",ans + 1); } int main() { while(1) { n = read(); m = -1; if(n == 0) break; for(int i = 1;i <= n;++i) a[i] = read(); for(int i = 1;i < n;++i) a[i] = a[i + 1] - a[i] + 100; m = 200; get_sa(); get_height(); erfen(); } return 0; } /* 30 25 27 30 34 39 45 52 60 69 79 69 60 52 45 39 34 30 26 22 18 82 78 74 70 66 67 64 60 65 80 */