后缀数组小结

#前言 一道模板题 后缀数组(SA)是一个比较强大的处理字符串的算法,是有关字符串的比较基础是吗?算法,因此必须掌握 实现主要有倍增和$DC3$,而我太弱了只学了倍增 #目录 ##知识点 ###1.基数排序+倍增 ###2.最长公共前缀Height ##一些要维护的东西 $s$:就是这个字符串,长度为$len$ $rank[i]$:表示$i到len$这个后缀在全部后缀中的排名 $sa[i]$:表示排名为$i$的后缀的首字符下标 $height[i]$:表示相邻两个排名的后缀的的最长公共前缀的长度 #基数排序+倍增构造SA 首先基数排序:就是低位到高位一位一位桶排,详见baidu 怎么排序? 若是一位一位来,那不就成了暴力了 因此咱们要倍增排序 具体来讲,对于一个长度为$2^k$的字符串,咱们能够把它当作是两个长度为$2^{k-1}$的字符串组成的,那么就至关于它有两个关键字,咱们就从长度为$1$开始,每次对这两个关键字排序不就好了 一张古老的图 看代码c++

# include <bits/stdc++.h>
# define IL inline
# define RG register
# define Fill(a, b) memset(a, b, sizeof(a))
using namespace std;
typedef long long ll;
const int _(1e6 + 5);

int len, sa[_], t[_], a[_], rk[_], y[_];
char s[_];
//t是桶;a和s同样;rk就是rank;y是辅助rk的数组,和sa性质类似;sa就是sa

IL bool Cmp(RG int i, RG int j, RG int k){  return y[i] == y[j] && y[i + k] == y[j + k];  } //肯定两个子串是否相同

IL void Sort(){
	RG int m = 80; //初始字符集大小
	for(RG int i = 1; i <= len; ++i) ++t[rk[i] = a[i]];
	for(RG int i = 1; i <= m; ++i) t[i] += t[i - 1];
	for(RG int i = len; i; --i) sa[t[rk[i]]--] = i;  //初始一个字符的排序
	for(RG int k = 1; k <= len; k <<= 1){  //倍增
		RG int l = 0;
		for(RG int i = 0; i <= m; ++i) y[i] = 0;
		//先按第二关键字排序,y记下编号
		for(RG int i = len - k + 1; i <= len; ++i) y[++l] = i;  //越界的第二关键字没有,放前面
		for(RG int i = 1; i <= len; ++i) if(sa[i] > k) y[++l] = sa[i] - k;  //剩下的按顺序排
		//再按rk第一关键字排序
		for(RG int i = 0; i <= m; ++i) t[i] = 0;
		for(RG int i = 1; i <= len; ++i) ++t[rk[y[i]]];
		for(RG int i = 1; i <= m; ++i) t[i] += t[i - 1];
		for(RG int i = len; i; --i) sa[t[rk[y[i]]]--] = y[i];
		//用此次的rk更新下次的
		swap(rk, y); rk[sa[1]] = l = 1;
		for(RG int i = 2; i <= len; ++i) rk[sa[i]] = Cmp(sa[i], sa[i - 1], k) ? l : ++l;  //相同的rk同样
		if(l >= len) break;  //此时确定不用排了,你们都不一样
		m = l;  //记录下次排序的字符集大小
	}
}

int main(RG int argc, RG char* argv[]){
	scanf(" %s", s + 1); len = strlen(s + 1);
	for(RG int i = 1; i <= len; ++i) a[i] = s[i] - '0';
	Sort();
	for(RG int i = 1; i <= len; ++i) printf("%d ", sa[i]);
	return puts(""), 0;
}

有点难理解,多看几个博客就能够了 #最长公共前缀---Height 有个$sa$和$rank$并无什么卵用,这个时候就有$Height$这个美妙的东西 怎么求? 暴力求$O(n^2)$显然不行 那么这个时候要利用$h$的美妙性质:$h[i]≥h[i-1]-1$ 证实:设后缀$suf[k]和suf[i-1]$为两个相邻排名的后缀,它们的最长公共前缀就是$h[i-1]$ 同时去掉第一个字符,那么就是$suf[k+1]和suf[i]$,那它们两个的最长公共前缀显然就是$h[i-1]-1$ 因此$suf[i]$和排在它前面的后缀的最长公共前缀至少是$h[i-1]-1$算法

那么$h$就能够很快求出来了,那么$height$也就能很快求出来了数组

for(RG int i = 1; i <= len; ++i){
		h[i] = max(0, h[i - 1] - 1);
		if(rk[i] == 1) continue;
		while(a[i + h[i]] == a[sa[rk[i] - 1] + h[i]]) ++h[i];
	}
	for(RG int i = 1; i <= len; ++i) height[i] = h[sa[i]];

刚刚学代码比较丑,并且可能有问题

#应用 题去这里找 题去这里找 ##1.最长公共前缀 题:给定一个字符串,询问某两个后缀的最长公共前缀。 分析: 就是区间$height$的最小值,$RMQ$问题工具

重复子串:字符串A在字符串B中至少出现两次,则称A是B的重复子串。 ##2.可重叠最长重复子串 题:给定一个字符串,求最长重复子串,重复的两个子串能够重叠。 分析: 只须要求 height 数组里的最大值便可 ##3.不可重叠最长重复子串 题:给定一个字符串,求最长重复子串,重复的两个子串不能重叠。 分析: 考虑二分答案,每次二分一个答案$k$,把$height$按$>=k$分组 最长公共前缀不小于 k 的两个后缀必定在同一组。对于每组后缀,只须判断每一个后缀的 sa 值的最大值和最小值之差是否不小于k便可 另外:利用 height 值对后缀进行分组的方法很经常使用 ##4.可重叠的 k 次最长重复子串 题:给定一个字符串,求至少出现 k 次的最长重复子串,这 k 个子串能够重叠 分析: 也是二分答案+分组,判断有没有一个组的后缀个数不小于 k ##5.不相同的子串的个数 题:给定一个字符串,求不相同的子串的个数。 分析: 每一个子串必定是某个后缀的前缀,也就是求全部后缀不一样前缀的个数 每来一个后缀$suf(i)$就会有,$len-sa[i]+1$的新的前缀,又因为有$height$个重复的,那么就是$len-sa[i]+1-height$的贡献spa

还有不少用法见下面的文献.net


#参考文献 罗穗骞 --算法合集之《后缀数组——处理字符串的有力工具》code

相关文章
相关标签/搜索