后缀数组
后缀数组
构造后缀数组
后缀数组是将字符串所有的后缀存储到一个数组中,根据各个后缀的字典序排序后的结果数组。
\(sa[i]\) 表示排名为 \(i\) 的后缀编号,而 \(rk[i]\) 则表示编号为 \(i\) 的后缀的排名。
如何优化呢,考虑倍增,类似于 st 表的构建过程,转化为若干次双关键字排序。

此时我们的算法是 \(O(nlog^2n)\) 的,很猪猪,但已经能过洛谷板子题了。
void init()
{
rep(i,1,n) sa[i] = i, rk[i] = s[i];
for(int w = 1;w <= n;w <<= 1)
{
auto rp = [&](int x) { return std::make_pair(rk[x], rk[x + w]); };
std::sort(sa+1,sa+1+n,[&](int x,int y){return rp(x) < rp(y);});
int p = 0;
rep(i,1,n) tp[sa[i]] = rp(sa[i - 1]) == rp(sa[i]) ? p : ++p;
std::copy(tp + 1, tp + n + 1, rk + 1);
}
}
回归到排序本身,发现我们倍增每一层的排序都是双关键字排序,可以使用基数排序进行优化。
还可以发现,对于第二关键字,我们并不需要排序,只需要把超出字符串范围的编号放到头上,剩下的按原顺序放回去即可。
void qs()
{
rep(i,1,m) a[i] = 0;
rep(i,1,n) ++ a[rk[i]];
rep(i,1,m) a[i] += a[i - 1];
per(i,n,1) sa[a[rk[tp[i]]] --] = tp[i];
}
void init()
{
rep(i, 1, n) {rk[i] = c[i] - 'a' + 1, tp[i] = i;}
qs();
for(int w = 1, p = 0; p != n && w <= n; m = p, p = 0, w <<= 1)
{
rep(i, n - w + 1, n) tp[++p] = i;
rep(i, 1, n) if(sa[i] > w) tp[++p] = sa[i] - w;
qs(), std::swap(tp, rk), rk[sa[1]] = p = 1;
rep(i, 2, n) rk[sa[i]] = (tp[sa[i]] == tp[sa[i - 1]] && tp[sa[i] + w] == tp[sa[i - 1] + w]) ? p : ++p;
}
}
\(O(nlogn)\) 并不是后缀排序的理论最优复杂度,还可以使用 SA-IS 或者 DC3 做到线性复杂度,由于笔者太菜,这里就跳过了。
查询子串
使用后缀数组可以 \(O(|S|log|T|)\) 查询主串 \(S\) 中是否存在子串 \(T\) ,由于后缀数组具有单调性,二分即可,比较两个字符串的字典序需要 \(O(|S|)\) ,总复杂度也就是 \(O(|S|log|T|)\) 。
height 数组
先引入概念 \(LCP\) 表示两个字符串的最长公共前缀。
height 数组就表示 \(lcp(sa[i],sa[i-1])\) ,也就是第 \(i\) 名和它前一名的后缀的最长公共前缀。
下文将 height 数组简称为 he。
先引入一个 Lemma ,\(he[rk[i]] \ge he[rk[i-1]] - 1\) 。
证明可以看:https://oi-wiki.org/string/sa
由于增量始终大于 \(-1\) ,所以可以线性处理出 \(he\) 数组。
查询两个子串的 LCP
这个式子可以感性理解,如果 \(he\) 一直大于某个数,前这么多位就一直没变过,反之,由于后缀已经排好序了,不可能变了之后变回来。
此时该问题转化为 RMQ 问题。
不同子串的数量
考虑所有子串都是某个后缀的前缀,将所有后缀排序。考虑对于相同的子串,在其出现的最大的后缀中记录贡献。于是对于每一个后缀,出现在后缀排序中下一个后缀的前缀不会被记录贡献,而没有出现的前缀一定会被记录贡献。
比较一个字符串的两个子串的大小关系
如果比较的是 \(𝐴 =𝑆[𝑎...𝑏]\) 和 \(𝐵 =𝑆[𝑐..𝑑]\) 的大小关系。
若 \(𝑙𝑐𝑝(𝑎,𝑐) \ge min(|𝐴|,|𝐵|)\),\(𝐴 < 𝐵 ⟺ |𝐴| < |𝐵|\)。
否则,\(𝐴 <𝐵 ⟺ 𝑟𝑘[𝑎] <𝑟𝑘[𝑐]\)。

浙公网安备 33010602011771号