后缀数组SA
相关链接
模板
struct SuffixArray{
int n, m;
string s;
vector<int> x, y, c, sa, rk, height;
explicit SuffixArray(string copy_s){
s = copy_s;
init();
get_sa();
get_height();
};
void init(){
n = s.size();
s = " " + s;
m = 500;
x.resize(2 * n + 1);
y.resize(2 * n + 1);
c.resize(max(m, n) + 1);
sa.resize(n + 1);
rk.resize(n + 1);
height.resize(n + 1);
}
void get_sa(){ // 倍增法+桶排序 O(nlogn) 求sa[]
int i, j, k;
// 初始化,按第一个字母排序
for(i = 1; i <= n; i ++) c[x[i] = s[i]] ++;
for(i = 1; i <= m; i ++) c[i] += c[i - 1];
for(i = n; i >= 1; i --) sa[c[x[i]] --] = i;
// 初始时的 sa 仅是将所有后缀按照第一个字母的字典序升序排序得到的排名,不同后缀的排名可能相同
for(k = 1; k <= n; k<<=1){ // 倍增,每次执行完当前轮后得到所有后缀按 长度为2^k的前缀(后面若有缺失的部分用空字符填补) 的字典序排序后的 sa
// 按第二关键字排序
for(i = 0; i <= m; i ++) c[i] = 0;
for(i = 1; i <= n; i ++) y[i] = sa[i];
for(i = 1; i <= n; i ++) c[x[y[i] + k]] ++;
for(i = 1; i <= m; i ++) c[i] += c[i - 1];
for(i = n; i >= 1; i --) sa[c[x[y[i] + k]] --] = y[i];
// 按第一关键字排序
for(i = 0; i <= m; i ++) c[i] = 0;
for(i = 1; i <= n; i ++) y[i] = sa[i];
for(i = 1; i <= n; i ++) c[x[y[i]]] ++;
for(i = 1; i <= m; i ++) c[i] += c[i - 1];
for(i = n; i >= 1; i --) sa[c[x[y[i]]] --] = y[i];
// 把后缀放入桶数组
for(i = 1; i <= n; i ++) y[i] = x[i];
for(m = 0, i = 1; i <= n; i ++){
if(y[sa[i]] == y[sa[i - 1]] && y[sa[i] + k] == y[sa[i - 1] + k]) x[sa[i]] = m;
else x[sa[i]] = ++m;
}
if(m == n) break; // 所有后缀排名均不相同时,说明已经得到了正确的 sa[],可以提前退出
}
// rk[sa[i]] = i
for(int i = 1; i <= n; i ++) rk[sa[i]] = i;
}
void get_height(){ // O(n) 得到 height[]
// 利用性质:height[rk[i]] >= height[rk[i - 1]] - 1
for(int i = 1, k = 0; i <= n; i ++){
if(rk[i] == 1) continue;
if(k > 0) k --;
int j = sa[rk[i] - 1];
while(i + k <= n && j + k <= n && s[i + k] == s[j + k]) k ++;
height[rk[i]] = k;
}
}
};
P2408
Q:给定一个字符串,求不同的子串个数。
答案为 所有子串个数 - \(\sum height_{i}\)
证明:对本质不同的子串计数,核心在于保证无重复:对于每种子串,一定出现在排名连续的若干个后缀中,我们可以只在排名值最大的后缀累计贡献。
而对于任何一种子串,它会出现在这些后缀中任意两个排名相邻后缀的 lcp 中,进而对排名更高的后缀的 height 产生一次贡献。因此我们可以通过减掉这些后缀的所有 height,来消除这种子串的冗余贡献(只会剩下一次)。进而得证上述结论。
P4051
破环成链再跑 SA
P2852
Q:求一个字符串中 至少出现 \(k\) 次的子串 的最大长度。
显然可以二分答案 + 字符串 hash 解决,这里写一下后缀数组 SA 的做法。
在 SA 中,一对后缀的排名越相近,它们的相似度就越高,重合前缀的长度 (即 lcp)就越长。
而每个子串,可以看作是某个后缀的一个前缀。
因此,对于重复出现的子串,它们必然在一组排名 (rk) 连续的后缀中。
对于 height:
即排名为 \(i\) 的后缀与排名为 \(i-1\) 的后缀的最长公共前缀长度。
显然 lcp 函数是具有嵌套性质的,即:
因此求在原串中至少出现 \(k\) 次的子串的最大长度,本质上就是求 height 数组内 所有长度为 \(k - 1\) 的连续段内的最小值中的最大值,这可以通过对 height 数组做单调队列来解决。
P4341
Q:给定一个长度为 \(n\) 的 01 串,求所有出现次数 \(>1\) 的子串的出现次数,按照子串的字典序升序输出答案。\(n \le 3000\)
正常做法应该是 \(O(n^{2})\) 枚举所有子串建字典树,在每个子串的结尾位置记录 \(cnt\),然后先序遍历字典树(相当于按照字典序升序遍历所有可能的子串),找所有 \(cnt\) 值 \(>1\) 的子串即可。
这里写一种 SA 做法:由于只需要子串的出现次数 \(>1\),因此我们只需要找每一对排名相邻的后缀,并对它们的 \(lcp\) 的所有前缀计数就可以了。但是需要注意同一个后缀可能会重复找两次,需要注意去重。
P8023
Q:给定两个字符串,可以按照队列的方式取出任意一个字符串的首字符,最终合成一个字符串,求字典序最小的合成字符串。
每次贪心取出 当前两个字符串中字典序较小的字符串 的首字符,不断重复该过程直至取完所有字符。但是需要注意一个特判:当字典序较小的字符串恰好是字典序较大的字符串的前缀时,改为取字典序较大的字符串的首字符。
将两个字符串拼接在一起然后做 SA,为了实现特判需要提前在两个字符串结尾添加一个无穷大,然后再做拼接。然后每次取出某个字符串的首字符后,我们都会得到当前两个字符串的排名,排名的相对大小等价于字典序的相对大小,因此省去了暴力比较两个字符串字典序的过程,实现了优化。
P4070
Q:初始时字符串为空,多次操作,每次操作往字符串结尾添加一个字符,每次操作后求当前字符串的不同子串个数。
如果正序处理字符串,那么所有后缀都会发生变化,关于后缀数组 SA 的所有信息都会无规律地变化,这显然不是我们想看到的。而若倒序处理字符串,则每次相当于往开头加一个字符,所有原后缀都不会改变,只会添加一个新的后缀。又因为将字符串倒序不会改变本质不同子串的个数,因此我们不妨将整个字符串倒序,然后每次往开头加字符,尝试动态且高效地维护 height 数组。
由于 lcp 的嵌套性质,我们会发现一个结论:对于排名为 \(i\) 与排名为 \(j\) 的后缀,它们的 lcp 长度为:
在往开头加字符的过程中,每次都会产生一个新的后缀,且该后缀的排名已经通过 SA 预处理好了。考虑当前所有后缀的排名的有序集合,当前字符串的 height 数组显然由该有序集合离散化后排名相邻的一对后缀的 lcp 长度组成。
于是,加入一个后缀,height 数组会分别加入该后缀与原后缀集合中排名最相近(一小一大)的两个后缀的 lcp 长度,然后再减去这两个原后缀的 lcp 长度。这便是每次加入新后缀时整个 height 数组的变化,显然根据上述结论,height 数组的总和是可以利用 ST表维护 height 最小值 \(O(1)\) 维护的。
P2178
题意较繁琐,但与下一道题很类似,均需按照 height 值降序枚举,然后利用并查集维护区间集合。
CF1780G
Q:给一个字符串,求满足以下条件的所有子串的数量:该子串在原串中的出现次数是该子串长度的倍数。\(n \le 10^{6}\)
显然能想到枚举子串长度,然后调和级数枚举子串出现次数,复杂度 \(O(n\log n)\) 特别合理,但如何统计这样的子串是难点。
对原串求 SA 后,得到 height 数组。由 lcp 的嵌套性质可知,\(min_{i=l+1}^{r} \space height_{i}\) 表示排名在 \([l, r]\) 的所有后缀的 lcp 长度。
那么,若某个长度为 \(l\) 的子串在原串中出现恰好 \(k\) 次,则说明:在 height 数组中,一定存在一个长度恰好为 \(k-1\) 的区间(\(k-1\) 个连续的 height 对应 \(k\) 个排名连续的后缀),该区间内 height 的最小值 \(\geq l\)(注意该区间长度必须恰好为 \(k\),不能向外延展,也就是左右边界两侧的 height 值不存在或者 \(< l\))。
因此,对于长度为 \(l\) 的任意子串,我们只需要枚举它的出现次数 \(q * l\),然后在 height 数组中找长度恰好为 \(q*l-1\) 的区间,使得该区间的最小值 \(\ge l\)。设这样的区间数量为 \(cnt_{q*l-1}\),则答案累计 \(cnt_{q*l-1} * (q*l)\)。
于是,问题变成了如何维护关于 height 数组的区间长度桶 \(cnt\)。由于有效的 height 值必须 \(\ge\) 当前枚举的长度 \(l\),因此不难发现,升序枚举长度相当于删点,降序枚举长度相当于加点。而显然加点是比较好维护的:只需要一个并查集维护相邻区间的合并就行了。具体实现见 code。
P4248
Q:给定一个长度为 \(n\) 的字符串,令 \(T_{i}\) 表示它从第 \(i\) 个字符开始的后缀,求:
前面两个增量的贡献为定值 \(\frac{n(n+1)(n-1)}{2}\),问题等价于求:
由 \(height\) 数组的性质:\(lcp(s_{i\backsim n}, s_{j\backsim n}) = \min(height_{i+1 \backsim j})\),我们可以将计算上式转化为每个 \(height_{i}\) 作为最小值时的子区间数量之和,可以单调栈 \(O(n)\) 解决。具体细节见 code。

浙公网安备 33010602011771号