给定字符串s
,定义suffix(i)
表示以s[i]
开头的后缀,例如abcde
的后缀suffix(2)
= cde
,如果s
的长度为n
,则s
共有n
个后缀,整数i
表示后缀suffix(i)
,将这n
个后缀按照字典序从小到大排序,便形成了后缀数组sa
,其中sa[i] = j
表示排序后s
的后缀suffix(j)
排在位置i
。例如
s = abdcd
// s 的各个后缀
suffix(0) = abdcd
suffix(1) = bdcd
suffix(2) = dcd
suffix(3) = cd
suffix(4) = d
// 根据字典序从小到大排序
0: suffix(0) = abdcd
1: suffix(1) = bdcd
2: suffix(3) = cd
3: suffix(4) = d
4: suffix(2) = dcd
// sa 数组
sa[0] = 0
sa[1] = 1
sa[2] = 3
sa[3] = 4
sa[4] = 2
那么,我们需要一个高效且编码难度适中的算法求解sa
数组
参考博客:破解外星代码:详解后缀数组模板
int wa[N], wb[N], wv[N], ws[N];
inline bool cmp(int *r, int a, int b, int len, int n) {
if (r[a] != r[b]) return false;
a = (a + len < n ? r[a + len] : -1); // 我们是从 0 开始排名的,越界的情况下值为 -1
b = (b + len < n ? r[b + len] : -1);
return a == b;
}
void SA(char *s, 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] = s[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 = p = 1; p < n; j <<= 1, 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 (int i = 0; i < m; i++) ws[i] = 0;
for (int i = 0; i < n; i++) ws[wv[i] = x[y[i]]]++;
for (int i = 1; i < m; i++) ws[i] += ws[i - 1];
for (int i = n - 1; i >= 0; i--) sa[--ws[wv[i]]] = y[i];
for (t = x, x = y, y = t, x[sa[0]] = 0, p = i = 1; i < n; i++)
x[sa[i]] = cmp(y, sa[i - 1], sa[i], j, n) ? p - 1: p++;
}
}
此模板利用倍增法求解sa
数组。sa
数组表示s
各个后缀排序后的结果,为了方便描述,我们定义
suffix(i, k)
为字符串 s
以 s[i]
开始的长度为 k
的后缀,长度不足则用字符0
填充sa(k)
为 s
的所有 k
后缀的排序结果。先求 sa(2 ^ 0)
再求 sa(2 ^ 1)
…,如何在 sa(2 ^ i)
的基础上求出 sa(2 ^ (i + 1))
是倍增算法的核心,这个地方使用了基数排序,我们会在接下来的分析中进行说明,现在就开始逐步分析这份代码吧。
sa(1)
,求s
的各个1
后缀的排序结果,s
的1
后缀不就是s
的每个字符吗?
for (i = 0; i < m; i++) ws[i] = 0;
for (i = 0; i < n; i++) ws[x[i] = s[i]]++;
for (i = 1; i < m; i++) ws[i] += ws[i - 1];
for (i = n - 1; i >= 0; i--) sa[--ws[x[i]]] = i;
上面这段代码就是求sa(1)
的,我们先明确这几个变量的作用
x[]
: 记录 k
后缀的值,sa(k)
数组不就是各个k
后缀按照字典序从小到大排列形成的吗?那么值小的后缀在前,值大的在后。有了sa(k)
为啥还需要x[]
?sa(k)
排在前面的后缀的值是小于等于排在后面的后缀,仅仅使用sa(k)
我们无法判断到底是小于还是等于,因此需要增加一个x[]
,针对两个 k
后缀 i
、j
,根据 x[i]
和 x[j]
的大小关系,便可知道后缀 i
与后缀 j
的大小关系。此处 k = 1
,即 x[i]
表示字符 s[i]
的值,在 C/C++
中字符的值就是其对应的 ASCII
码,其值的大小关系符合字典序,因此在遍历字符的过程中,我们将1
后缀的值直接设置为字符的值,即x[i] = s[i]
m
: 我们利用计数排序将1
后缀排好序,m
就是我们有多少个不同的值,将各个字符排序我们一般用128
ws[]
: 计数排序用的统计数组,记录各个值出现的次数sa[]
: 记录 k
后缀从小到大排序的结果,此时k = 1
介绍完各个变量,现在就梳理这段代码了
ws[]
全置为 0
s
的每个字符,对应字符值出现次数加一,此处我们用 x[i] = s[i]
将1
后缀的值设置为对应的字符值s
的每个字符,suffix(i, 1)
的值为x[i]
,对应的排名为--ws[x[i]]
,从后往前遍历是为了对于后缀值相同的后缀,排在后面的后缀对应的排名更大,--ws[x[i]]
先减是因为我们排名是从0
开始计的。如何在 sa(1)
的基础上求解 sa(2)
? 我们注意到 suffix(i, 2)
是由两部分组成:suffix(i, 1)
、suffix(i + 1, 1)
, 这两个后缀的值分别为 x[i]
、x[i + 1]
,设这两部分的值分别为第一关键字和第二关键字,此处我们利用基数排序,先根据第二关键字从小到大排序,再根据第一关键字从小到大排序,最后便可得到所有的 2
后缀的排序结果。
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 (int i = 0; i < m; i++) ws[i] = 0;
for (int i = 0; i < n; i++) ws[wv[i] = x[y[i]]]++;
for (int i = 1; i < m; i++) ws[i] += ws[i - 1];
for (int i = n - 1; i >= 0; i--) sa[--ws[wv[i]]] = y[i];
这段代码用于求解 sa(2)
。同样的步骤,我们先分析各个变量的作用。
j
:表示 sa(j)
,即 j
后缀的排序结果已经计算出来了,正在求解 sa(2 * j)
,此时 j = 1
,sa(1)
已经计算出来,正在计算 sa(2)
。ws[]
: 同上,用于计数排序的统计数组。y[]
: 所有的 2
后缀仅根据第二关键字从小到大排序的结果。p
: 生成y
数组的下标计数。wv[]
: 按照第二关键字从小到大排序的后缀的第一关键字的值。x[]
: 记录所有的 suffix(i, 1)
, 即所有的 1
后缀的值,这里仅使用,后续才更新。分析完变量后,我们开始分析这段代码。
生成 y[]
,一开始不太明白第一条代码,看了几篇博客后才明白它的含义,y[]
的定义是所有的 2
后缀仅根据第二关键字从小到大排序的结果,那么如果第二关键字不存在,则将第二关键字视为最小,因此这些后缀排在 y[]
的前面,i >= n - j
的后缀的第二关键字均不存在,因此排在前面。
然后遍历整个 sa(1)
,相当于从小到大遍历第二关键字,其对应的 2
后缀起始位置则为 sa[i] - j
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;
求出 y[]
后,我们已经按照第二关键字将 2
后缀排序好了,接下来再根据第一关键字将后缀从小到大排序,则能得到 sa(2)
的结果。
for (int i = 0; i < m; i++) ws[i] = 0;
for (int i = 0; i < n; i++) ws[wv[i] = x[y[i]]]++;
for (int i = 1; i < m; i++) ws[i] += ws[i - 1];
for (int i = n - 1; i >= 0; i--) sa[--ws[wv[i]]] = y[i];
0
y[]
的顺序)遍历每个 2
后缀,将后缀对应的第一关键字对应的值(x[y[i]]
)出现次数加一,并在遍历的过程中,将后缀对应的值用 wv[]
保存,即 wv[i] = x[y[i]]
sa(1)
一样,为保证稳定排序,我们按第二关键字排序的逆序(y[]
的逆序)遍历每个 2
后缀,后缀对应的第一关键字的值为 x[y[i]]
即第2
步保存的 wv[i]
,遍历后变得出了 sa(2)
sa(2)
已经求出来了,下一步就是求解 sa(4)
了,但是在求 sa(4)
之前我们需要做准备工作,4
后缀 suffix(i, 4)
也是由两部分构成suffix(i, 2)
和 suffix(i + 2, 2)
,两个关键字分别为 2
后缀的值,但是目前 x[]
是各个1
后缀的值,我们需要将 x[]
更新为所有 2
后缀的值。更新代码如下
for (t = x, x = y, y = t, x[sa[0]] = 0, p = i = 1; i < n; i++)
x[sa[i]] = cmp(y, sa[i - 1], sa[i], j, n) ? p - 1: p++;
2
后缀的值是根据 1
后缀的值以及sa(2)
求出来的,sa(2)
中排在前面的2
后缀的值小于等于排在后面的2
后缀,那么我们从前往后遍历sa(2)
数组,然后再根据1
后缀的值判断前后两个2
后缀的值是否相等,这样就可以求出所有 2
后缀的值了。这里需要注意一点,后缀的值其实就是指符合字典序的大小关系,我们可以从 0
开始赋值,从前往后遍历 sa(2)
数组时,如果当前后缀和前一个后缀相同,则赋同样的值,否则赋更大的值。判断两个后缀是否相同其实很简单,只要比较两个后缀对应的第一、第二关键字是否相同即可。代码如下
inline bool cmp(int *r, int a, int b, int len, int n) {
if (r[a] != r[b]) return false;
a = (a + len < n ? r[a + len] : -1); // 我们是从 0 开始排名的,越界的情况下值为 -1
b = (b + len < n ? r[b + len] : -1);
return a == b;
}
由于 x[]
是需要更新的值,同时在更新的过程中又需要旧x[]
中的值,因此我们交换x[]
,y[]
,那么在更新的过程中使用y[]
的数据即可。更新完毕后,p
的值就是所有2
后缀中不同的值的数量,继续进行下一轮循环。
for (j = p = 1; p < n; j <<= 1, m = p)
j
从1
变成 2
,m
更新为 p
(即不同的后缀值的个数),注意循环结束的条件是 p == n
,即不同的后缀值的个数有 n
个,当出现n
个不同后缀值时,说明各个后缀已经排好序了,因此便可退出循环。
至此,分析完毕,求出 sa
数组后,我们便可利用 sa
做一些有趣的事了。