后缀数组——强大的字符串处理工具
在字符串处理当中,后缀树和后缀数组都是非常有力的工具。其实后缀数组是后缀树的一个非常精巧的替代品,它比后缀树容易编程实现,能够实现后缀树的很多功能而时间复杂度也不太逊色,并且,它比后缀树所占用的空间小很多。可以说,在信息学竞赛中后缀数组比后缀树要更为实用。——摘自《后缀数组——处理字符串的有力工具》(罗穗骞)
前置知识
在学习后缀数组之前,应该了解以下几个定义:
- 子串:
字符串
- 后缀:
字符串的后缀即为从某个位置
- 字符串的大小:
大部分题目中字符串的大小即为为他们的字典序大小(如果你不知道什么是字典序,请出门左转百度一下)
前方高能,非战斗人员请撤离
- 后缀数组
sa[] :
后缀数组
即:
- 名词数组
rk[]
排名数组
显然,
- 基数排序
具体请看这期洛谷日报
倍增法
求后缀数组通常有两种办法:倍增法和DC3算法。
其中倍增算法时间复杂度为
那么倍增算法究竟是怎么实现的?
倍增算法的内容简单来说是通过每次利用已经排好序的长度为
注意在排序过程中
-
对于
sa[] 数组来说,当两个子串完全相同时,按他们第一个字符在原串位置的前后定义大小 -
而对于
rk[] 数组来说,当两个子串完全相同时,为了保证下一次进行基数排序的准确性,他们的排名也相同。
而对长度为
看不懂没关系,可以联系下面具体来看
详细的流程
放代码前先放下宏定义
#define rep(i, a, b) for(register int i = (a); i <= (b); i++)
#define foi(i, a, b) for(register int i = (a); i >= (b); i--)
- 最开始要排序的子串长度为一,那么他们的第一关键字大小即为他们的字典序大小,第二关键字大小即为他们的前后顺序。进行基数排序
sz = 80;//sz表示一共有多少个字符,用来进行基数排序
rep(i, 1, n) rk[i] = s[i] - '0' + 1, tmp[i] = i;
//tmp[]即为存储第二关键字的桶
Qsort();
其中基数排序代码如下:
void Qsort() {
rep(i, 0, sz) bin[i] = 0;//bin[]为桶
rep(i, 1, n) bin[rk[i]]++;
rep(i, 1, sz) bin[i] += bin[i - 1];
foi(i, n, 1) sa[bin[rk[tmp[i]]]--] = tmp[i];
}
- 倍增
倍增长度
for (register int j = 1; j <= n; j <<= 1) {
由于再次进行排序时,我们的第一关键字即为上一次的相应位置子串的排名,而第二关键字为上一次对应位置
int p = 0;
rep(i, n - j + 1, n) tmp[++p] = i;//处理没有第二关键字的后j个子串
rep(i, 1, n) if (sa[i] > j) tmp[++p] = sa[i] - j;
//注意第i个子串第二关键字是上一次第i+j个子串的排名
Qsort();
求出sa[]数组后,下面我们要对
因为对相同的子串处理方法不一样,所以我们无法简单使用
不过由于不同的地方仅仅是子串相同的情况,所以我们只需要在利用上面的式子时进行特判即可
而两个子串
rep(i, 2, n) {
int v0 = sa[i - 1], v1 = sa[i], v00 = -1, v01 = -1;
if (v0 + j <= n) v00 = rk[v0 + j];
if (v1 + j <= n) v01 = rk[v1 + j];
if (rk[v0] == rk[v1] && v00 == v01) tmp[sa[i]] = p;
else tmp[sa[i]] = ++p;
}
rep(i, 1, n) rk[i] = tmp[i];//更新rk
sz = p; if (p == n) break;//两个优化
现在我们就得出了但是有什么用啊?
height[] 数组
对于后缀数组来说,最精髓的地方就是
-
最长公共前缀:
lcp(i, j) 表示suf[i] 和suf[j] 的最长公共前缀的长度 -
定义
LCP(i, j) = lcp(sa[i], sa[j])
关于
-
LCP(i,j)=LCP(j,i) -
LCP(i,i)=length(suf(sa[i]))=n-sa[i]+1
这样的话我们就可以仅仅讨论
-
height[]$数组:$height[i]=LCP(i - 1, i)
即排名第
-
h[]$数组:$h[i] = height[rk[i]]
即第
在这里给出两个性质:
-
LCP(i, j) = min(height[k])\ \ (i<k\leq j)
也可以写成
严谨的证明可以在百度上查找,这里仅仅叙述我个人的理解:(才不是我懒得写了)
对于
而在第一个字符相同时,第二个字符相同的可能性与上述规律一样
例如字符串
a
aba
ababa
ba
baba
这样的话,如果
这样就说明
而
故
证毕。
-
h[i] >= h[i - 1] - 1
因为第
这样的话
求height[] 方法
通过结论2,我们可以从
-
先令
k = h[i-1]-1 (定义k为递推出的h[i]) -
int j, k = 0;
rep(i, 1, n) {
if (k) k--;//步骤一
j = sa[rk[i] - 1];
while (s[i + k] == s[j + k]) k++;//步骤2
height[rk[i]] = k;//别忘了h[i]的定义
}
整理在一起就是模板了:
void Qsort() {
rep(i, 0, sz) bin[i] = 0;
rep(i, 1, n) bin[rk[i]]++;
rep(i, 1, sz) bin[i] += bin[i - 1];
foi(i, n, 1) sa[bin[rk[tmp[i]]]--] = tmp[i];
}
void SA() {
sz = 80;
rep(i, 1, n) rk[i] = s[i] - '0' + 1, tmp[i] = i;
Qsort();
for (register int j = 1; j <= n; j <<= 1) {
int p = 0;
rep(i, n - j + 1, n) tmp[++p] = i;
rep(i, 1, n) if (sa[i] > j) tmp[++p] = sa[i] - j;
Qsort();
p = 1; tmp[sa[1]] = 1;
rep(i, 2, n) {
int v0 = sa[i - 1], v1 = sa[i], v00 = -1, v01 = -1;
if (v0 + j <= n) v00 = rk[v0 + j];
if (v1 + j <= n) v01 = rk[v1 + j];
if (rk[v0] == rk[v1] && v00 == v01) tmp[sa[i]] = p;
else tmp[sa[i]] = ++p;
}
rep(i, 1, n) rk[i] = tmp[i];
sz = p; if (p == n) break;
}
int j, k = 0;
rep(i, 1, n) {
if (k) k--;
j = sa[rk[i] - 1];
while (s[i + k] == s[j + k]) k++;
height[rk[i]] = k;
}
}
应用
讲了这么半天,后缀数组究竟怎么应用?这里讲解几经典的应用:
- 最长公共子串codevs3160
题目大意:给出字符串A,B求A和B的最长公共子串的长度
注意子串的可以理解为后缀的前缀,那么我们可以将A和B链接在一起,求起点在分别在两个字符串的后缀的最长公共前缀的长度的最大值。
唯一的问题是:起点在A中的后缀与起点在B中的后缀的最长公共前缀是由A的后缀与B的前缀组成。
对于上面一种请况,我们可以在AB中间插入A和B中不会出现的字符,保证最长公共前缀前缀不会穿过AB
这样我们从
主函数如下:
int cmp2(int i, int fi) {
return (sa[i] > fi && sa[i - 1] < fi)
|| (sa[i - 1] > fi && sa[i] < fi);
}
int main() {
/* freopen(".in", "r" ,stdin);
freopen(".out", "w" ,stdout);*/
scanf("%s", s + 1); n = strlen(s + 1);
s[n + 1] = '1';
scanf("%s", s + n + 2);
int fi = n + 1; n = strlen(s + 1);
SA(); int ans = 0;
rep(i, 2, n) ans = max(ans, (cmp2(i, fi) ? height[i] : 0));
printf("%d", ans);
return 0;
}
时间复杂度
- 最长不可重叠公共子串POJ1743
题目大意:给定一个序列
s ,求最长的相似子串的长度,若长度小于5 ,输出零。其中相似的的子串定义为其中一个子串的每一项都加上同一个数后与另一个子串完全相同
我们对每一项都与它前面一项求差,这样求相似子串就转变为了求公共子串,求差后的公共子串长度为
这里我们可以二分答案
我们从
这时,如果组内最靠前的和最靠后的位置相距大于等于
主函数如下:
int check(int len) {
int mx = sa[1], mi = sa[1];
rep(i, 2, n) {
if (height[i] >= len)
mx = max(mx, sa[i]), mi = min(mi, sa[i]);
else mx = mi = sa[i];
if (mx - mi >= len + 1) return 1;
}
return 0;
}
int search(int l, int r) {
int mid;
while (l < r) {
mid = l + r + 1 >> 1;
if (check(mid)) l = mid;
else r = mid - 1;
}
return l;
}
int main() {
while(scanf("%d", &n) == 1 && n != 0) {
n--;
rep(i, 0, n) ss[i] = re();
rep(i, 1, n) s[i] = ss[i] - ss[i - 1];
SA();
int ans = search(1, n / 2);
printf("%d\n", ans < 4 ? 0 : ans + 1);
}
return 0;
}
- 最长可重叠的至少重复
x 次子串POJ3261
和第二题一样,二分答案
从
当这个组里一共有
主程序:
int check(int len) {
int mx = 1, mi = 1;
rep(i, 2, n) {
if (height[i] >= len)
mx = max(mx, i), mi = min(mi, i);
else mx = mi = i;
if (mx - mi >= k - 1) return 1;
}
return 0;
}
int search(int l, int r) {
int mid;
while (l < r) {
mid = l + r + 1 >> 1;
if (check(mid)) l = mid;
else r = mid - 1;
//printf("%d ", mid);
}
return l;
}
int main() {
n = re(), k = re();
rep(i, 1, n) s[i] = re();
SA();
printf("%d", search(1, n));
return 0;
}
- 本质不同的子串个数SPOJ705(洛谷)
因为子串就是后缀的前缀,这样本题就变为后缀之间不相同的前缀个数,按排名考虑每一个后缀的前缀对答案的贡献。
这个后缀的长度即为其前缀个数,而重复出现的前缀的数量即为
若还有其他后缀与该后缀有公共前缀,一定会出现在其前面的后缀时进行的的计算中减去
最后的式子即为
主程序:
int main() {
int T = re();
while (T--) {
scanf("%s", s + 1);
n = strlen(s + 1); int ans = 0;
SA();
rep(i, 1, n) ans += (sa[i] - height[i]);
printf("%d\n", ans);
rep(i, 1, n) s[i] = '\0';
}
return 0;
}
总结
实际上后缀数组更像是一种思想,在做题时要充分利用后缀数组的性质来进行阶梯