算法学习:后缀数组(SA)
【参考博客】
【定义】
【后缀】从第i位到字符串结尾的子串
【解决问题】
从而解决
...................在字符串中找子串
...................比较子串关系
...................查找不同子串的数目
一般来说都是解决
字符串和子串关系的问题
【算法学习】
后缀数组能够在 nlogn的时间复杂度内求取出以下数组
【注意】明白字符串包含字符的范围
SA [] 储存,第 i 个数字表示的是字典序第 i 大的后缀是以 SA_i 开始的后缀
即 “排第几的是哪个后缀”
rank [] 储存,从第i位开始的后缀的字典序排名是 i
即 “某个后缀排第几个”
对SA的求取,我们可以看作对所有的后缀进行排序
而这个排序显然如果直接莽的话肯定T,所以我们需要另外一种方法
这里使用基数排序+倍增的方法进行优化
将所有的后缀进行排序得到SA,这是我们的目的
基数排序,是对两个关键字的元素进行排序从而达到线性复杂度的方法
显然,在当两个后缀第一个字符相等的情况下,我们不可避免的去用第二个字符进行比较
这里我们需要注意到一个事情
第 i 个后缀的第二个字符是第 i + 1 的第一个字符
那这和直接莽好像没有什么区别?
所以我们就用到了倍增,每次确定后缀的以前 2 ^ k 长度的字符串排序的顺序
然后通过这个,就能够求出 2 ^ ( k + 1 ) 长度的后缀的前缀
通过,第 i 位的和第 i + k 位的,于是就能求出来
因为第一个字符有可能一样,导致有两个后缀排名相等
所以总排名数和后缀长度不同
所以长度为2时同理
而当所有总排名和后缀长度相同时,这个时候就找到了所有
下面是对使用到的各个数组的意义的描述:
c[] 桶,记录第 i 位的元素有多少个
x[] 后缀 i 的第一关键字,所以最开始是等于第 i 位字符
y[] 第二关键字排名第 i 的字符串,第一关键字的位置
【代码】
首先求出所需要的几个数组的初值
//初始化 int n = strlen(s+1); int m = 128; //m只需要大于 ascii(‘z')即可 //因为第一步并不知道桶的规模 for (int i = 1; i <= n; i++) ++c[x[i] = s[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) //k是之前提到的,每次枚举的字符串长度 { 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 = 1; i <= m; i++) c[i] = 0; //清空桶 for (int i = 1; i <= n; i++) c[x[i]]++; //放入第一关键字 //第一关键字是已经算好的 //第一次是初始化时计算好的 //第二次的计算过程在下面 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] = 0; //在保证第一关键字的同时使用第二关键字排序 //从后往前,第二关键字靠后的会被先剔除掉 //考虑下数组的操作 swap(x, y); //这里是为了保存上一步得到的y,没有太多其他意思 x[SA[1]] = 1; num = 1; for (int i = 2; i <= n; i++) x[SA[i]] = (y[SA[i]] == y[SA[i - 1]] && y[SA[i] + k] == y[SA[i - 1] + k]) ? //这里注意回忆SA的意义 //比较的是和其排名相近的元素,也是最有可能两个关键字都相同的元素 //当字符的第二关键字,前后都相同时 //说明没有新的元素,所以不加 //如果有,则加 ? num : ++num; if (num == n) break; m = num; }
【模板题】
【luogu P3809】
求一个字符串的SA
#include<cstdio> #include<cstring> #include<iostream> #include<algorithm> using namespace std; const int MAXN = 1000010; char s[MAXN]; int SA[MAXN]; int x[MAXN], c[MAXN], y[MAXN]; void get_SA(char *s) { //初始化 int n = strlen(s + 1); int m = 128; for (int i = 1; i <= n; i++) ++c[x[i] = s[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) //如果长度大于k y[++num] = SA[i] - k; //把第一个放进去 for (int i = 1; i <= m; i++) c[i] = 0; for (int i = 1; i <= n; i++) c[x[i]]++; //x[i]是第一关键字 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] = 0; swap(x, y); x[SA[1]] = 1; num = 1; for (int i = 2; i <= n; i++) x[SA[i]] = (y[SA[i]] == y[SA[i - 1]] && y[SA[i] + k] == y[SA[i - 1] + k]) ? num : ++num; if (num == n) break; m = num; } return; } int main() { scanf("%s", s + 1); get_SA(s); int n = strlen(s + 1); for (int i = 1; i <= n; i++) printf("%d ", SA[i]); return 0; }
【扩展】