00:00:00
后缀数组(SA) 笔记
过于困难了。
用于解决一个字符串
一、求 sa 数组
swap 前的数组意义:
:排名为 ,长度为 的子串的编号。 :起点为 ,长度为 的子串的排名。 :第二关键字排名为 ,长度为 的子串的起点。
swap 后的数组意义:
:起点为 ,长度为 的子串的排名。 :swap 前的 。
算法流程是:先倍增枚举一个
预处理一个字符
s: | a | b | a | b | a |
x: | 97 | 98 | 97 | 98 | 97 |
sa: | 5 | 3 | 1 | 4 | 2 |
k = 1
s: | a+b | b+a | a+b | b+a | a+_ |
oldx:| 1 | 2 | 1 | 2 | 1 |
y: | 5 | 2 | 4 | 1 | 3 |
sa: | 5 | 1 | 3 | 2 | 4 |
x: | 2 | 3 | 2 | 3 | 1 |
k = 2
s: |ab+ab|ba+ba|ab+a_|ba+__|a_+__|
oldx:| 2 | 3 | 2 | 3 | 1 |
y: | 4 | 5 | 3 | 1 | 2 |
sa: | 5 | 3 | 1 | 4 | 2 |(
由于只有两个关键字,所以可以用基数排序,比较难理解的是代码,可结合注释理解。
点击查看代码
cpp
void build() {
for (int i = 1; i <= n; i++)
c[x[i] = s[i]]++;
// 初始只有一个字符时,每个子串的排名就是它的 ascii 码。
for (int i = 2; i <= m; i++)
c[i] += c[i - 1];
// 根据计数排序,求前缀和后,值 x 的排名是 c[x-1]+1 ~ c[x]。
for (int i = n; i; i--)
sa[c[x[i]]--] = i;
// 计数排序倒出来。
for (int k = 1;; k <<= 1) {
int num = 0;
for (int i = n - k + 1; i <= n; i++)
y[++num] = i;
// 这些以 i 开头的子串没有第二关键字,排名最先。
for (int i = 1; i <= n; i++)
if (sa[i] > k)
y[++num] = sa[i] - k;
// 如果排名为 i 的长度为 k 的子串,前面还至少有 k 个字母,那么就可以成为第二关键字接上去。
// 由于 sa 的定义,这样加入的排名是正确的。
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; i--)
sa[c[x[y[i]]]--] = y[i];
// 同理
swap(x, y);
// 交换后,y[i] 的定义变为了第一关键字的起始点为 i 的排名。
x[sa[1]] = num = 1;
// 初始化,排名为 1 的子串的排名是 1。
for (int i = 2; i <= n; i++)
x[sa[i]] = (y[sa[i]] == y[sa[i - 1]] and y[sa[i] + k] == y[sa[i - 1] + k] ? num : ++num);
// 如果排名为 i 的子串的第一关键字和第二关键字(即排名为 i 的子串后面 k 个字母的子串)的排名都
// 和前一个子串一样,那么就说明这两个子串相同,无需增加排名。
m = num;
// 还有多少个不同的子串。
if (n == m)
break;
// 没有相同的,结束。
}
}二、求 height 数组
定理
证明:当
时,右边 ,成立。 否则,由于
,那么可以得到后缀 和后缀 有长度大于 的 LCP,假设这个 LCP 表示为 ( 是 , 是一个 的串)。 那么后缀
就是 , 就是 ,后缀 就是 ,根据 定义,有 。 由于
仅比后缀 排名小一位,而已经存在 ,所以 的开头也一定会是 。 所以
的大小至少是 ,也就是 。
这里有一个感性理解图片。
所以至少是上一步减一。
然后对着定理暴力求即可。
应用
求两个后缀的 LCP
搞个 ST 表即可。
