后缀数组 学习笔记

后缀数组 学习笔记


定义

包含 \(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)

直接应用

最小循环位移

寻找子串

我们有离线和在线两种做法。

假设主串为 \(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|})\)

从字符串首尾取字符最小化字典序

结合 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 数组。

应用

两子串最长公共前缀

很直观,我们可以发现答案就是:

\[\operatorname{lcp}(sa_i,sa_j) = \min_{k=i+1}^{j} {ht_k} \\ \]

那么变成了 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 个串的公共子串

长度不小于 k 的公共子串对数

这题有两个做法。

法 1

先求出长度范围为全部的,再减去范围 \(< k\) 的。

第一步很简单,第二步只要把 \(ht\) 全部对 \(k-1\)\(\min\),在重复一遍第一步的就可以。

法 2

其实就是直接求,过程相差不大。

最长回文串

正反串相接,中间用不出现字符连接,后缀排序、求 \(ht\),然后枚举回文中心,查询正反串的 LCP 即可。

本质不同子串数目统计

这也很直观。

假设统计的字符串为 \(|S|\)\(n\) 为其大小。

那么对其后缀排序,然后求出 \(ht\),最后答案就是 \(\frac{n(n+1)}{2}-\sum_{i=2}^n ht_i\)

至少 k 次的子串的最大长度

先对其后缀排序,然后求出 \(ht\)

二分

二分最大长度,检查有没有连续 \(k-1\)\(ht\) 的值小于二分的 \(mid\)

单调队列

直接滑动窗口保持 \(k-1\) 的大小,中间存储 \(ht_i\),求解也比较直观。

至少不重叠地出现了两次的子串最大长度

连续的若干个相同子串

大致思路是枚举 peiord 长度 \(L\),按照这个长度对全串分块,查询相邻两块的 LCP 与 LCS(Longest Common Suffix,最长公共后缀),对他们长度之和进行判断。

这部分总时间复杂度为调和级数 \(O(n\log_2{n})\)

  • 例题 1:SP687 REPEATS - Repeats - 洛谷 (luogu.com.cn)

    这题是查询重复次数最多的连续重复子串的重复次数。

    1. \(O(n\log_2{n}\ln{n})\)

      那么我们先按照上面的思路做,可以得到一个做法:

      二分答案,即二分重复次数,然后枚举 peiord 长度 \(L\) 检验有没有连续 \(mid\) 个相邻块满足 \(\operatorname{lcp} + lcs -1 \ge L\)

    2. \(O(n\ln{n})\)

      明显上面那个二分没有必要。我们固定 \(L\),然后找到连续最多次的块即可。

    3. \(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\) 都大于等于子串长度。

结合数据结构

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)

本题结合并查集

posted @ 2025-08-21 14:18  Add_Catalyst  阅读(14)  评论(0)    收藏  举报