后缀数组 学习笔记
后缀数组 学习笔记
定义
包含 \(sa\) 和 \(rk\) 两个数组,其含义如下:
-
\(sa_i\) 表示:将所有后缀按字典序排序后,在第 \(i\) 位的后缀的第一位下标。
-
\(rk_i\) 表示:将所有后缀按字典序排序后,第一位下标为 \(i\) 的后缀的排名。
其中 \(sa\) 是我们所说的后缀数组(Suffix Array),而 \(rk\) 是重要的辅助数组。
后缀排序
我们考虑如何快速求出后缀数组。
暴力 \(O(n^2\log_2{n})\)
直接对字符串或数组排序。
\(O(n\log_2^2{n})\)
法 1:Hash + 二分
我们可以在快速排序的时候用 Hash 二分出第一个不同的位置,然后判断即可。
但是这个方法无法优化。
法 2:倍增
假设我们现在得到了所有 \(s[i,\min(n,i+2^w-1)]\) 的排名,暂存在 \(rk_i\) 中。
那么我们就可以凭借这些排名得到所有 \(s[i,\min(n,i+2^{w+1}-1)]\) 的排名。
将 \(rk_i,rk_{i+2^w}\) 分别作为第一、二关键字用快排进行排序即可。
\(O(n\log_2{n})\)
由按关键字排序,可以想到将偏序排序换成计数类排序,我们把快排换成基排即可。
常数优化
如果不加常数优化,这个算法甚至跑不过 \(O(n\log_2^2{n})\)。
第二关键字
发现其实对于第二关键字的排序,只需要把长度不足的放到数组最前面,然后把剩余的按第二关键字放到后面。
值域
发现可以动态维护值域,减少不必要的遍历,同时值域到达 \(n\) 时就不需要要继续排序了。
template<const int N>struct SA {
int n;
int sa[N],osa[N],rk[N<<1],ork[N<<1],cnt[N];
int &operator [](int i) { return sa[i]; }
int &operator ()(int i) { return rk[i]; }
void Osa(int w) {
int cur(0);
FOR(i,n-w+1,n)osa[++cur]=i;
FOR(i,1,n)if(sa[i]>w)osa[++cur]=sa[i]-w;
}
void Sa(int m) {
RCL(cnt+1,0,int,m);
FOR(i,1,n)++cnt[rk[i]];
FOR(i,1,m)cnt[i]+=cnt[i-1];
DOR(i,n,1)sa[cnt[rk[osa[i]]]--]=osa[i];
}
int Unique(int w) {
int p(0);
CPY(ork+1,rk+1,int,n);
FOR(i,1,n)rk[sa[i]]=(ork[sa[i]]==ork[sa[i-1]]&&ork[sa[i]+w]==ork[sa[i-1]+w]?p:++p);
return p;
}
void Constr(char *S) {
n=strlen(S+1),RCL(sa+1,0,int,n),RCL(rk+1,0,int,n);
FOR(i,1,n)osa[i]=i,rk[i]=S[i];
for(int w(1),m((Sa(128),Unique(0))); w<n&&m<n; Osa(w),Sa(m),m=Unique(w),w<<=1);
}
};
\(O(n)\)
大概率是不怎么会用。
SA-IS
后缀数组简介 - OI Wiki (oi-wiki.org)。
DC3
后缀数组简介 - OI Wiki (oi-wiki.org)。
最优原地后缀排序算法
最优原地后缀排序算法 - OI Wiki (oi-wiki.org)。
直接应用
最小循环位移
-
例题:P4051 [JSOI2007] 字符加密 - 洛谷 (luogu.com.cn)。
拆环成链,再后缀排序即可。
寻找子串
我们有离线和在线两种做法。
假设主串为 \(T\),我们要的模式串为 \(S_i,i\in[1,n]\)。
离线
我们将 \(T\) 与所有 \(S_i\) 拼接起来,并且中间用不出现、大于所有出现字符且递增的字符连接,最后再加上一个最大的字符,然后后缀排序。
查询的时候找到每个子串的首字母在 \(sa\) 中的位置,然后不断往前比较,可以找到所有与它相同的子串位置,如果用后文的 \(height\) 数组,我们可以将查询这个过程优化到 \(O(|T|+ \sum_{i=1}^n |S_i|)\)。
在线
对 \(T\) 进行后缀排序,然后对于模式串 \(S_i\),我们直接在 \(T\) 的 SA 中二分查找,即可找到与它相同的子串的范围,可以用 Hash 优化到 \(O(|T| + \sum_{i=1}^n |S_i|\log_2{|S_i|}\log_2{|T|})\)。
从字符串首尾取字符最小化字典序
-
例题:P2870 [USACO07DEC] Best Cow Line G - 洛谷 (luogu.com.cn)。
正反串相接进行后缀排序,然后查询位置优化排序即可。
结合 height 数组
前置知识:LCP
LCP 即 Longest Common Prefix,最长公共前缀。
对于字符串 \(S,T\),\(\operatorname{lcp}(S,T)\) 为它们最长公共前缀的长度。
在后文,我们设 \(\operatorname{lcp}(i,j)\) 表示以下标 \(i,j\) 作为首字母的后缀的最长公共前缀的长度。
定义
定义 \(ht_i = \operatorname{lcp}(sa_{i-1},sa_{i})\),对于 \(ht_1\),我们视作 \(ht_1=0\)。
引理
\(ht_{rk_i} \ge ht_{rk_{i-1}}-1\),证明略。
那么就可以 \(O(n)\) 求 height 数组。
应用
两子串最长公共前缀
很直观,我们可以发现答案就是:
那么变成了 RMQ 问题。
比较子串大小
先利用“两子串最长公共前缀”的方法比较最长公共前缀后面的字符,如果没有的话直接比较 \(rk\)。
求最小循环节
先后缀排序,再求出 \(ht\)。
枚举 \(n\) 的约数 \(i\),如果满足 \(rk_{i+1} = rk_1 + 1\) 和 \(ht_{rk_1} = n-i\),那么我们就可以知道 \(n-i\) 是串的最大 border,\(i\) 是其最小循环节。
最长公共子串
把多个要求的串拼起来,中间用不同的不会出现的字符连接,然后后缀排序再求出 \(ht\)。
如果串数较少,只有 \(k\) 个,我们可以用类似尺取的方法做到 \(O(nk)\) 处理。
否则我们可以二分长度,检验函数也极其简单。
变体:最长 k 个串的公共子串
-
例题:UVA11107 Life Forms - 洛谷 (luogu.com.cn)
同理,二分时限制个数即可。
长度不小于 k 的公共子串对数
这题有两个做法。
法 1
先求出长度范围为全部的,再减去范围 \(< k\) 的。
第一步很简单,第二步只要把 \(ht\) 全部对 \(k-1\) 取 \(\min\),在重复一遍第一步的就可以。
法 2
其实就是直接求,过程相差不大。
最长回文串
正反串相接,中间用不出现字符连接,后缀排序、求 \(ht\),然后枚举回文中心,查询正反串的 LCP 即可。
本质不同子串数目统计
这也很直观。
假设统计的字符串为 \(|S|\),\(n\) 为其大小。
那么对其后缀排序,然后求出 \(ht\),最后答案就是 \(\frac{n(n+1)}{2}-\sum_{i=2}^n ht_i\)。
- 例题:SP694 DISUBSTR - Distinct Substrings - 洛谷 (luogu.com.cn),SP705 SUBST1 - New Distinct Substrings - 洛谷 (luogu.com.cn),P2408 不同子串个数 - 洛谷 (luogu.com.cn)。
至少 k 次的子串的最大长度
先对其后缀排序,然后求出 \(ht\)。
二分
二分最大长度,检查有没有连续 \(k-1\) 个 \(ht\) 的值小于二分的 \(mid\)。
单调队列
直接滑动窗口保持 \(k-1\) 的大小,中间存储 \(ht_i\),求解也比较直观。
至少不重叠地出现了两次的子串最大长度
-
例题:P2743 [USACO5.1] 乐曲主题Musical Themes - 洛谷 (luogu.com.cn)
二分目标串长度 \(mid\),然后检验的时候,只要看中间都满足 \(ht_i \ge mid,\forall i\in(l,r]\) 的 \([l,r]\),是否有 \(\max_{i=l}^r sa_i - \min_{i=l}^r sa_i > mid\) 即可。
连续的若干个相同子串
大致思路是枚举 peiord 长度 \(L\),按照这个长度对全串分块,查询相邻两块的 LCP 与 LCS(Longest Common Suffix,最长公共后缀),对他们长度之和进行判断。
这部分总时间复杂度为调和级数 \(O(n\log_2{n})\)。
-
例题 1:SP687 REPEATS - Repeats - 洛谷 (luogu.com.cn)
这题是查询重复次数最多的连续重复子串的重复次数。
-
\(O(n\log_2{n}\ln{n})\):
那么我们先按照上面的思路做,可以得到一个做法:
二分答案,即二分重复次数,然后枚举 peiord 长度 \(L\) 检验有没有连续 \(mid\) 个相邻块满足 \(\operatorname{lcp} + lcs -1 \ge L\)。
-
\(O(n\ln{n})\):
明显上面那个二分没有必要。我们固定 \(L\),然后找到连续最多次的块即可。
-
\(O(n\ln{n})\):
运用 border 与 period 转化定理,当 \(s[1,n-L] = s[L+1,n]\),则 \(s\) 存在长为 \(L\) 的 period。
我们考虑固定 \(L\),枚举相邻两块的最后一个点 \(p,q\),即满足 \(L|p,L|q,q=p+L\)。
求出 \(l=|LCS(pre_p,pre_q)|,r=|LCP(suf_p,suf_q)|\),说明对于 \(suf_{p-l+1},suf_{q-l+1}\) 有长为 \(l+r-1\) 的 LCP ,即 \(s[p-l+1,p+r+1] = s[q-l+1,q+r+1]\),那么 period 出现次数为 \(\frac{l+r-1}{L}\)。
-
-
例题 2:P1117 [NOI2016] 优秀的拆分 - 洛谷 (luogu.com.cn)
这道题与上一道有所不同,因为这题是计数题,而上一题只要求最值,所以这题可能用不了 border 与 period 的转化定理。
我们依旧是考虑固定 \(L\),枚举相邻两块的最后一个点 \(p,q\),即满足 \(L|p,L|q,q=p+L\)。
然后再设 \(p’ = p-1,q' = q-1\),求出 \(l = |LCS(pre_{p’},pre_{q'})|, r = |LCP(suf_{p},suf_{q})|\)。
我们知道如果 \(l+r \ge L\),他们就有 \(AA\)(\(BB\)) 的结构存在,我们可以求出这个结构的开端和结尾,然后差分统计,最后再在输出答案时,相邻的相乘求和即可。
查找子串出现次数
在 SA 上二分即可,找到最大的区间使其满足中间的 \(ht\) 都大于等于子串长度。
-
例题:P3649 [APIO2014] 回文串 - 洛谷 (luogu.com.cn)
本题结合 Manacher 即可。
结合数据结构
P9482 [NOI2023] 字符串 - 洛谷 (luogu.com.cn)
本题结合扫描线。
首先可以很简单地打出一个暴力:建出正反串 SA,比较 \(i\) 与 \(rev(i+2l-1)\) 的 \(rk\),再求 \(LCP\),如果 \(rk_i < rk_{rev(i+2l-1)}\) 且 \(LCP\) 长度 \(<l\),即为合法。
-
那么对于 \(rk_i < rk_{rev(i+2l-1)}\) 的部分,我们建出 SA 后再倒序扫描线就可以了。
-
然后我们考虑减去 \(LCP\) 长度 \(\ge l\) 的部分。
先对 SA 构建动一点手脚:构建用的字符串改为 \(S+\operatorname{char}(\inf)+rev(S)+\operatorname{char}(-\inf)\)。
然后发现经过我们的一番改动,需要被减去的 \(s[i,i+2l-1]\) 就构成了一个偶回文串。
用 Manacher 求出所有偶回文串,假设现在左回文串中心为 \(i\),最长半回文长度为 \(len\),即 \(s[i-len+1,i+len]\) 是一个以 \(i\) 为左回文中心的最长偶回文串。
需要被减去的 \(s[i,i+2l-1]\) 还要满足 \(s_{i-len}>s_{i+len+1}\),因为这样才会有 \(rk_i < rk_{rev(i+2l-1)}\),
那么我们求出了每个左回文中心对应的最长半回文长度,就可以再进行一遍扫描线:修改是斜率为 \(-1\) 的线段,而查询则是斜率为 \(1\) 的直线,我们把图旋转 45 度,再做扫描线即可。
P2178 [NOI2015] 品酒大会 - 洛谷 (luogu.com.cn)
本题结合并查集。

浙公网安备 33010602011771号