Loading

简单的后缀数组

Powered by Codex

后缀数组:倍增算法

后缀数组(Suffix Array,简称 SA)解决的是这样一个问题:给定字符串 \(s\),将它的所有后缀按字典序排序。

本文使用 1 下标。设字符串长度为 \(n\),从位置 \(i\) 开始的后缀记作

\[s[i\ldots n]. \]

我们维护两个数组:

  • \(sa[i]\):字典序第 \(i\) 小的后缀的起点;
  • \(rk[i]\):从位置 \(i\) 开始的后缀,其字典序排名。

二者互为逆排列,即

\[sa[rk[i]]=i,\qquad rk[sa[i]]=i. \]

例如字符串 banana 的后缀为:

起点 后缀
1 banana
2 anana
3 nana
4 ana
5 na
6 a

排序后依次为 a, ana, anana, banana, na, nana,所以

\[sa=[6,4,2,1,5,3]. \]

倍增的基本思想

假设我们已经知道每个长度为 \(w\) 的子串的排名。要比较从 \(i\) 开始、长度为 \(2w\) 的子串,可以将其拆成两半:

\[s[i\ldots i+2w-1] =s[i\ldots i+w-1]+s[i+w\ldots i+2w-1]. \]

于是,每个位置 \(i\) 都对应一个二元组

\[(rk[i],rk[i+w]). \]

先比较第一关键字,再比较第二关键字,就等价于比较长度为 \(2w\) 的子串。排序后重新编号,便能得到新一轮排名。依次令

\[w=1,2,4,8,\ldots \]

直到所有后缀排名互不相同,后缀数组就构造完成了。

初始时 \(w=1\),单个字符的字符码就是它的第一轮排名。

如果 \(i+w>n\),第二段已经越过字符串末尾。我们约定空串的排名为 \(0\);所有非空串的排名都从 \(1\) 开始,因此空串一定比任何非空串小。

原代码没有显式判断 \(i+w\le n\),而是直接访问 \(rk[i+w]\)。这是有意为之:\(rk\) 是全局数组,未赋值的位置会被初始化为 \(0\),正好可以作为空串的排名。这里访问的下标虽然可能超出 \([1,n]\),但只要仍在数组分配的范围内,就不属于 C++ 意义下的数组越界。后文会精确分析需要预留多少空间。

为什么使用计数排序

如果每一轮都直接对二元组排序,一轮需要 \(O(n\log n)\),共进行 \(O(\log n)\) 轮,总复杂度会变成 \(O(n\log^2 n)\)

注意两个关键字都是不超过 \(n\) 的整数,可以使用计数排序。经典实现先利用上一轮的 \(sa\) 顺序生成按第二关键字有序的 \(id\),再对第一关键字做一次计数排序。

生成第二关键字有序的 \(id\)

对于本轮长度 \(w\)

  1. 起点 \(n-w+1\ldots n\) 的第二段越界,第二关键字均为 \(0\),它们应放在最前面;
  2. 如果 \(sa[i]>w\),那么起点 \(sa[i]-w\) 的第二关键字是 \(rk[sa[i]]\)。由于 \(sa\) 已按 \(rk\) 排好,按 \(sa\) 的顺序加入这些起点,第二关键字自然有序。

随后,只需按第一关键字 \(rk[id[i]]\) 稳定计数排序,即可得到按二元组有序的新 \(sa\)

每轮复杂度为 \(O(n)\),轮数为 \(O(\log n)\),所以:

  • 时间复杂度:\(O(n\log n)\)
  • 空间复杂度:\(O(n)\)

重新编号

排序后,相邻两个位置 \(x=sa[i-1]\)\(y=sa[i]\) 属于同一类,当且仅当它们的两个关键字都相等:

\[ork[x]=ork[y] \]

\[\operatorname{rank}(x+w)=\operatorname{rank}(y+w), \]

其中 \(ork\) 保存上一轮排名;当 \(p>n\) 时,全局数组中对应的 \(ork[p]\) 尚未被赋值,值为 \(0\)

必须使用旧排名 \(ork\) 比较,不能一边写新 \(rk\) 一边拿 \(rk\) 比较,否则本轮尚未完成就会混入新排名。

代码实现

下面的程序读入一个不含空白字符的字符串,输出 \(sa[1\ldots n]\)\(ht[1\ldots n]\)。代码保持原模板的写法:全局数组、1 下标、\(rk/ork/buc/id\) 命名,并直接利用全局数组的零初始化处理不存在的第二关键字。

# include <bits/stdc++.h>

const int N=1000010;

char s[N];
int n;

namespace SA{

int sa[N],rk[N],ork[N],buc[N],id[N],ht[N];

inline void init(void){
	int mx=128;

	// 初始时按单个字符排序;题目字符集应保证字符码不超过 mx。
	std::fill(buc+1,buc+1+mx,0);
	for(int i=1;i<=n;++i) ++buc[rk[i]=s[i]];
	for(int i=1;i<=mx;++i) buc[i]+=buc[i-1];
	for(int i=1;i<=n;++i) sa[buc[rk[i]]--]=i;

	for(int w=1,cc=0;w<=n;w<<=1,mx=cc,cc=0){
		// 第二关键字为 0 的后缀排在最前面。
		for(int i=n-w+1;i<=n;++i) id[++cc]=i;

		// 按上一轮 sa 的顺序加入,其第二关键字已经有序。
		for(int i=1;i<=n;++i) if(sa[i]>w) id[++cc]=sa[i]-w;

		memset(buc,0,(mx+1)*sizeof(int));
		memcpy(ork,rk,sizeof(ork));
		cc=0;
		for(int i=1;i<=n;++i) ++buc[ork[i]];
		for(int i=1;i<=mx;++i) buc[i]+=buc[i-1];

		for(int i=n;i;--i) sa[buc[ork[id[i]]]--]=id[i];

		// && 短路保证第二关键字只在第一关键字相等时比较,此时最多访问到 ork[n+1]。
		for(int i=1;i<=n;++i)
			rk[sa[i]]=(ork[sa[i]]==ork[sa[i-1]]&&ork[sa[i]+w]==ork[sa[i-1]+w])?cc:++cc;
		if(cc==n) break;
	}

	// Kasai:按后缀起点顺序求相邻后缀的 LCP。
	for(int i=1,k=0;i<=n;++i){
		if(k) --k;
		while(s[i+k]==s[sa[rk[i]-1]+k]) ++k;
		ht[rk[i]]=k;
	}

	return;
}

} // namespace SA

int main(void){
	scanf("%s",s+1);
	n=strlen(s+1);

	SA::init();

	for(int i=1;i<=n;++i) printf("%d%c",SA::sa[i]," \n"[i==n]);
	for(int i=1;i<=n;++i) printf("%d%c",SA::ht[i]," \n"[i==n]);

	return 0;
}

越界分析

原代码使用 && 连接两次比较。C++ 会对 && 短路求值:只有第一关键字相等,即

\[ork[sa[i]]=ork[sa[i-1]] \]

成立时,才会计算第二关键字的比较。

上一轮的排名描述长度为 \(w\) 的子串。如果两个第一关键字相等,而其中某个后缀的长度小于 \(w\),那么这个后缀已经在串尾结束,另一个后缀要么更长,要么长度不同,两者在第一关键字中就已经能够分出大小,不可能排名相等。因此,进入第二次比较时,两个后缀的长度都至少为 \(w\)。对于其中任意一个起点 \(p\),都有

\[n-p+1\ge w, \]

所以

\[p+w\le n+1. \]

因此,没有被短路的第二关键字比较最多只会访问到 \(ork[n+1]\),恰好比有效排名区间 \([1,n]\) 多一位。全局数组中的 \(ork[n+1]\)\(0\),正好表示空串。为保证这次访问仍在数组内,只需满足

\[n+1<N. \]

这里也可以从排序结果理解:如果第一关键字已经不同,表达式立即短路,代码根本不会计算可能到达 \(2n\) 的下标。若忽略短路机制,只根据 \(sa[i]\le n\)\(w\le n\) 作估计,虽然会得到 \(sa[i]+w\le 2n\),但这个上界实际上不可达。

\(i=1\) 时,表达式还会访问 \(sa[0]\)\(ork[sa[0]]\)。全局数组满足 \(sa[0]=0\)\(ork[0]=0\),因此第一个后缀一定会建立新的排名。这里向左只访问到下标 \(0\),没有出现负下标。

生成 \(id\) 时,只在 \(sa[i]>w\) 的情况下计算 \(sa[i]-w\),故结果至少为 \(1\)。另一方面,由于 \(w\le n\),区间左端点 \(n-w+1\) 也至少为 \(1\)。所以这一部分不会访问下标 \(0\) 或负下标。

还要注意:代码中的输入语句本身不限制输入长度。若题目不能保证 \(n\le 10^6\),应根据题目上限调整 \(N\) 或给输入增加长度限制;这与倍增时利用 \(ork\) 的零区域是两件不同的事。

Height 数组

后缀数组给出了所有后缀的顺序,但很多应用还需要知道相邻后缀有多相似。定义

\[ht[i]=\operatorname{lcp}(sa[i-1],sa[i]), \]

其中 \(\operatorname{lcp}(x,y)\) 表示后缀 \(s[x\ldots n]\)\(s[y\ldots n]\) 的最长公共前缀长度,并规定 \(ht[1]=0\)

例如 banana 的后缀数组为

\[sa=[6,4,2,1,5,3]. \]

相邻后缀依次比较,可以得到

\[ht=[0,1,3,0,0,2]. \]

为什么只需比较相邻后缀

对于任意两个排名 \(x<y\),有

\[\operatorname{lcp}(sa[x],sa[y]) =\min_{x<k\le y}ht[k]. \]

如果两个后缀的最长公共前缀长度为 \(L\),那么所有排在它们之间的后缀也具有这个长度为 \(L\) 的前缀,因此中间每一对相邻后缀的 LCP 都至少为 \(L\);而区间中一定存在一对相邻后缀,其 LCP 恰好为 \(L\),否则两端后缀的公共前缀还可以继续延长。

这个性质说明,\(ht\) 已经包含任意两个后缀的 LCP 信息。若以后需要多次询问,可以在 \(ht\) 上建立 RMQ;本文只讨论 \(ht\) 本身的线性求法。

Kasai 算法

\[h[i]=ht[rk[i]], \]

即后缀 \(i\) 与它在后缀数组中的前驱的 LCP。Kasai 算法利用下面的性质:

\[h[i+1]\ge h[i]-1. \]

设后缀 \(i\) 在后缀数组中的前驱为后缀 \(j\),且 \(h[i]=k\)。删除两个后缀的首字符后,后缀 \(i+1\) 与后缀 \(j+1\) 至少仍有 \(k-1\) 个相同字符。后缀 \(j+1\) 不一定恰好是后缀 \(i+1\) 的前驱,但它排在后缀 \(i+1\) 之前;根据字典序的连续性,后缀 \(i+1\) 与其真正前驱的 LCP 不会小于 \(k-1\)。因此上式成立。

于是按 \(i=1,2,\ldots,n\) 的顺序计算时,可以保留上一次的答案 \(k\)。每次先令

\[k\gets\max(k-1,0), \]

再从第 \(k+1\) 个字符开始继续比较。每次字符相等会使 \(k\) 增加,而每轮至多使 \(k\) 减少一次,所以所有 while 循环合计只会执行 \(O(n)\) 次,求 \(ht\) 的时间复杂度为 \(O(n)\)

哨兵与边界

原模板没有单独判断 \(rk[i]=1\)。当后缀 \(i\) 的排名为 \(1\) 时,它没有前驱,代码会取到

\[sa[rk[i]-1]=sa[0]=0. \]

由上面的不等式,循环开始时保留的 \(k\) 始终不超过真实的 \(h[i]\)。此时 \(h[i]=0\),所以 \(k\) 必然已经减到 \(0\)。又因为全局数组满足 \(s[0]=0\),而串内字符 \(s[i]\ne 0\)while 会立即停止,最终正确得到 \(ht[1]=0\)

\(rk[i]>1\) 时,被比较的是两个不同后缀。若它们一直相等到其中一个结束,较短后缀会先访问到 \(s[n+1]='\0'\),而另一个后缀仍指向串内的非零字符,比较立即停止。两个不同后缀不可能同时在同一轮到达末尾,因此不会在两个 \0 之间继续比较。这里最多向右多访问一位,即最多访问 \(s[n+1]\),仍只需满足

\[n+1<N. \]

需要注意,这个论证要求输入串中不含 \0,这对由 scanf("%s",s+1) 读入的普通字符串自然成立。如果处理的是任意整数序列,则应自行添加一个不与序列元素相同的哨兵,或者在 while 中显式判断边界。

典型应用例题

下面整理 OI Wiki 后缀数组页面中适合本阶段的经典问题。它们不要求新的后缀数组构造方法,重点是理解如何使用 \(sa\)\(rk\)\(ht\)

例 1:最小循环表示

给定长度为 \(n\) 的字符串 \(s\),在它的所有循环移位中求字典序最小者。

\[t=s+s. \]

每个循环移位都对应 \(t\) 中一个起点位于 \([1,n]\)、长度为 \(n\) 的子串。构造 \(t\) 的后缀数组后,从小到大枚举 \(sa[i]\),第一个满足 \(sa[i]\le n\) 的位置就是答案起点。

这里直接比较整个后缀仍然正确。两个循环移位若在前 \(n\) 个字符内已经不同,后缀顺序就是循环移位的顺序;若前 \(n\) 个字符完全相同,则两个循环移位本来就相同,任选其一即可。

这个问题只用到 \(sa\),可以作为后缀排序模板之后的第一道应用题。

例 2:在文本中查找模式串

给定文本串 \(s\) 和模式串 \(t\),求 \(t\) 是否在 \(s\) 中出现,或者求出所有出现位置。

所有以 \(t\) 为前缀的后缀在后缀数组中构成一个连续区间。可以在 \(sa\) 上二分:

  • 第一次二分求第一个字典序不小于 \(t\) 的后缀;
  • 第二次二分求第一个不以 \(t\) 为前缀、且字典序严格大于该前缀区间的后缀。

得到的排名区间内,每个 \(sa[i]\) 都是一次匹配的起点。一次朴素比较需要 \(O(|t|)\),所以单次询问复杂度为

\[O(|t|\log n+\mathrm{occ}), \]

其中 \(\mathrm{occ}\) 是输出的出现次数。若询问很多,还可以利用相邻后缀 LCP 避免二分过程中重复比较,但这不属于本文的基础范围。

例 3:比较两个子串

给定两个子串 \(s[l_1\ldots r_1]\)\(s[l_2\ldots r_2]\),比较它们的字典序。

设两个后缀的最长公共前缀为

\[L=\operatorname{lcp}(l_1,l_2), \]

两个子串长度分别为

\[len_1=r_1-l_1+1,\qquad len_2=r_2-l_2+1. \]

\(L\ge\min(len_1,len_2)\),则较短的子串字典序更小;若长度也相同,则两者相等。否则,差异出现在公共前缀后的第一个字符,直接比较 \(s[l_1+L]\)\(s[l_2+L]\) 即可。

由前文结论,假设 \(rk[l_1]<rk[l_2]\),则

\[L=\min_{rk[l_1]<k\le rk[l_2]}ht[k]. \]

因此在 \(ht\) 上建立 ST 表后,可以在 \(O(1)\) 时间求 LCP,并在 \(O(1)\) 时间比较任意两个子串。

例 4:不同子串个数

后缀 \(sa[i]\) 一共有

\[n-sa[i]+1 \]

个非空前缀。按后缀数组顺序加入这个后缀时,其中长度不超过 \(ht[i]\) 的前缀已经由前面的后缀贡献过,所以它新贡献

\[n-sa[i]+1-ht[i] \]

个不同子串。答案为

\[\sum_{i=1}^{n}\bigl(n-sa[i]+1-ht[i]\bigr) =\frac{n(n+1)}2-\sum_{i=1}^{n}ht[i]. \]

注意答案可能达到 \(\Theta(n^2)\),应使用 64 位整数。

例 5:出现至少两次的最长子串

任意出现至少两次的子串,必然是某两个后缀的公共前缀。若两个后缀的 LCP 为 \(L\),那么它们之间相邻后缀的某个 LCP 至少为 \(L\)。因此答案就是

\[\max_{2\le i\le n}ht[i]. \]

若还要输出这个子串,可以记录最大值所在的下标 \(p\),输出 \(s[sa[p]\ldots sa[p]+ht[p]-1]\)

例 6:出现至少两次且不重叠的最长子串

在例 5 的基础上,要求两次出现不能重叠。二分答案长度 \(L\)。所有 \(ht[i]\ge L\) 的连续段,对应一组拥有相同长度为 \(L\) 前缀的后缀。

对每一组维护后缀起点的最小值与最大值。如果存在一组满足

\[\max sa-min sa\ge L, \]

就能选出两次间距至少为 \(L\) 的出现,它们互不重叠。一次检查为 \(O(n)\),总复杂度为 \(O(n\log n)\)

例 7:出现至少 \(K\) 次的最长子串

二分答案长度 \(L\)。若某个连续区间中有至少 \(K-1\)\(ht\) 值均不小于 \(L\),那么对应的 \(K\) 个后缀具有一个长度至少为 \(L\) 的公共前缀。

等价地,枚举长度为 \(K-1\) 的 Height 区间,检查是否存在

\[\min_{j=i+1}^{i+K-1}ht[j]\ge L. \]

基础实现可以二分 \(L\) 并线性扫描连续段,复杂度为 \(O(n\log n)\)。洛谷 P2852 就是这一模型在整数序列上的版本。

例 8:两个字符串的最长公共子串

给定 \(s\)\(t\),取一个在两串中均未出现的分隔符 #,拼成

\[u=s+\texttt{\#}+t. \]

构造 \(u\) 的后缀数组。答案一定来自后缀数组中相邻、且分别起始于 \(s\)\(t\) 的两个后缀。因此枚举 \(i=2,3,\ldots,|u|\),若 \(sa[i-1]\)\(sa[i]\) 属于不同原串,就用 \(ht[i]\) 更新答案。

为什么只需看相邻后缀?假设某个来自 \(s\) 的后缀与某个来自 \(t\) 的后缀具有长度为 \(L\) 的公共前缀,那么所有具有这个前缀的后缀在 \(sa\) 中形成连续段。只要该段同时包含两种来源,其中必然存在一对来源不同的相邻后缀。

分隔符会在第一次跨过两串边界时终止匹配,因此答案不会错误地延伸到另一段字符串中。

进阶例题

下面三道题比前面的典型应用更综合,但核心仍然是把字符串关系转化为后缀之间的 LCP,再利用 Height 数组的结构进行统计。

例 9:洛谷 P1117 [NOI2016] 优秀的拆分

题目链接:https://www.luogu.com.cn/problem/P1117

如果一个字符串可以写成 \(AA\),其中 \(A\) 非空,就称它为一个平方串。题目要求统计字符串中形如

\[AABB \]

的子串个数,其中 \(AA\)\(BB\) 都是平方串。

  • \(f[i]\) 表示以位置 \(i\) 结尾的平方串个数;
  • \(g[i]\) 表示以位置 \(i\) 开始的平方串个数。

若一个优秀拆分在位置 \(i\)\(i+1\) 之间断开,那么左侧平方串有 \(f[i]\) 种选择,右侧平方串有 \(g[i+1]\) 种选择。因此答案为

\[\sum_{i=1}^{n-1}f[i]g[i+1]. \]

问题变为:如何统计所有平方串。

枚举平方串的一半长度

枚举 \(|A|=L\)。再枚举一组相距 \(L\) 的对齐位置 \(q\)\(q+L\),其中

\[q=1,1+L,1+2L,\ldots,qquad q+L\le n. \]

\[x=\operatorname{lcp}(q,q+L), \]

并令 \(y\) 表示从 \(q-1\)\(q+L-1\) 开始向左最多能匹配多少个字符,也就是这两个前缀的最长公共后缀。\(y\) 可以在反串上转化为一次 LCP 查询。

在区间

\[[q-y,q+x-1] \]

中,都有

\[s[t]=s[t+L]. \]

因此,起点为 \(p\)、一半长度为 \(L\) 的平方串合法,当且仅当区间 \([p,p+L-1]\) 完全落在上述匹配区间中,即

\[q-y\le p\le q+x-L. \]

同一个平方串可能被相邻的两个 \(q\) 重复发现。为了使每个起点只归属于一个对齐位置,再限制

\[q-L+1\le p\le q. \]

所以本次枚举贡献的平方串起点区间为

\[\left[\max(q-y,q-L+1),\ \min(q+x-L,q)\right]. \]

若左端点不大于右端点,就对 \(g\) 的这个区间整体加 \(1\);对应的结束位置为 \(p+2L-1\),所以同时对 \(f\) 的平移区间整体加 \(1\)。使用差分数组即可在 \(O(1)\) 时间完成一次区间加。

对于固定的 \(L\),需要枚举 \(O(n/L)\) 个对齐位置,所以总枚举次数为

\[\sum_{L=1}^{\lfloor n/2\rfloor}O\left(\frac nL\right)=O(n\log n). \]

在原串和反串上分别预处理后缀数组与 LCP 的 RMQ 后,每次求 \(x,y\) 都是 \(O(1)\),总复杂度为 \(O(n\log n)\)

实现时要特别注意,\(x\) 不能跨过字符串右端点,\(y\) 不能跨过左端点;用后缀 LCP 转换后,这些限制通常由后缀长度自然保证。区间端点仍应与合法起点范围 \([1,n-2L+1]\) 取交集,避免差分数组写到无效位置。

例 10:洛谷 P2178 [NOI2015] 品酒大会

题目链接:https://www.luogu.com.cn/problem/P2178

每个后缀起点 \(i\) 有一个权值 \(a_i\)。对于每个长度 \(L\),题目要求:

  1. 有多少对不同后缀的 LCP 不小于 \(L\)
  2. 在这些后缀对中,\(a_i a_j\) 的最大值是多少。

把后缀数组中的每个排名看成一个点。对于 \(i=2,3,\ldots,n\),在排名 \(i-1\)\(i\) 之间连一条权值为 \(ht[i]\) 的边。

\[\operatorname{lcp}(sa[x],sa[y])=\min_{x<k\le y}ht[k] \]

可知,对于固定的 \(L\),两个排名对应的后缀 LCP 不小于 \(L\),当且仅当它们在只保留边权不小于 \(L\) 的图中连通。

因此可以令 \(L\)\(n-1\) 递减到 \(0\),逐步加入所有满足 \(ht[i]=L\) 的边,并用并查集维护连通块。

设本次合并的两个连通块大小分别为 \(siz_x\)\(siz_y\)。合并前不连通、合并后连通的新后缀对数为

\[siz_x\cdot siz_y. \]

把它累加到当前后缀对总数中,就得到 LCP 不小于当前 \(L\) 的后缀对数。

为了求最大权值乘积,对每个连通块维护其中 \(a_i\) 的最小值 \(mn\) 和最大值 \(mx\)。乘法在两个区间端点处取得极值,因此两个块之间可能产生的最大乘积是下面四项的最大值:

\[\max\{mn_xmn_y,\ mn_xmx_y,\ mx_xmn_y,\ mx_xmx_y\}. \]

四项都要计算:当一个连通块内全是负数、另一个块内全是正数时,最优答案可能是绝对值最小的负数,不能只比较 \(mn_xmn_y\)\(mx_xmx_y\)。用这四项更新全局最大值,然后合并两个块的 \(mn,mx\) 即可。

若某个 \(L\) 没有新边加入,它的答案与 \(L+1\) 相同。若当前没有合法后缀对,题目规定最大乘积输出 \(0\)。排序 Height 边需要 \(O(n\log n)\);也可以按 \(ht\) 的整数值开桶,将这一部分优化为 \(O(n)\)。并查集合并的复杂度为 \(O(n\alpha(n))\)

例 11:洛谷 P4248 [AHOI2013] 差异

题目链接:https://www.luogu.com.cn/problem/P4248

题目要求计算所有无序后缀对 \((i,j)\) 的差异之和:

\[\sum_{1\le i<j\le n} \left(|S_i|+|S_j|-2\operatorname{lcp}(i,j)\right), \]

其中 \(S_i=s[i\ldots n]\),所以 \(|S_i|=n-i+1\)

先计算长度部分。每个后缀长度会与其余 \(n-1\) 个后缀各配对一次,因此

\[\sum_{1\le i<j\le n}(|S_i|+|S_j|) =(n-1)\sum_{i=1}^{n}(n-i+1) =\frac{n(n+1)(n-1)}2. \]

接下来只需求所有后缀对的 LCP 之和。按后缀数组中的排名枚举 \(x<y\),有

\[\operatorname{lcp}(sa[x],sa[y]) =\min_{x<k\le y}ht[k]. \]

\[b[i]=ht[i+1],\qquad 1\le i<n. \]

那么所有后缀对的 LCP 之和,恰好等于数组 \(b\) 的所有非空子数组最小值之和。

这个量可以用单调栈在线性时间内计算。对于每个位置 \(i\),找出:

  • 左侧最近的、严格小于 \(b[i]\) 的位置 \(L_i\)
  • 右侧最近的、小于等于 \(b[i]\) 的位置 \(R_i\)

采用一侧严格、一侧非严格,是为了让含有相同最小值的子数组只归属于其中一个位置。以 \(b[i]\) 作为所归属最小值的子数组共有

\[(i-L_i)(R_i-i) \]

个,因此

\[\sum_{1\le x<y\le n}\operatorname{lcp}(sa[x],sa[y]) =\sum_{i=1}^{n-1}b[i](i-L_i)(R_i-i). \]

最终答案为

\[\frac{n(n+1)(n-1)}2 -2\sum_{i=1}^{n-1}b[i](i-L_i)(R_i-i). \]

构造后缀数组需要 \(O(n\log n)\),计算 Height 数组和单调栈均为 \(O(n)\)。所有乘法与答案都必须使用 64 位整数;长度部分的数量级达到 \(O(n^3)\)

基础练习

下面的题目均未出现在开头给出的题目选讲链接中,并按所需技巧大致从简单到复杂排列。

1. 洛谷 P3809【模板】后缀排序

题目链接:https://www.luogu.com.cn/problem/P3809

直接构造并输出后缀数组。适合检查倍增、计数排序、重新编号和下标处理是否正确。建议先独立写出只求 \(sa\) 的版本,再加入 \(ht\)

2. 洛谷 P2408 不同子串个数

题目链接:https://www.luogu.com.cn/problem/P2408

一个长度为 \(n\) 的字符串共有

\[\frac{n(n+1)}2 \]

个带位置的非空子串。按后缀数组顺序考虑后缀 \(sa[i]\):它提供 \(n-sa[i]+1\) 个前缀,其中前 \(ht[i]\) 个已经在前面的后缀中出现。因此不同子串个数为

\[\sum_{i=1}^{n}\bigl(n-sa[i]+1-ht[i]\bigr) =\frac{n(n+1)}2-\sum_{i=1}^{n}ht[i]. \]

这是 \(ht\) 最直接的应用。

3. POJ 2774 Long Long Message

题目链接:https://vjudge.net/problem/POJ-2774

求两个字符串的最长公共子串。将它们用一个从未出现过的分隔符拼接,构造后缀数组。答案一定来自后缀数组中相邻、且分别属于两个原串的后缀,因此枚举 \(ht[i]\) 取最大值即可。

分隔符必须与原字符集不同,并且不能把跨过分隔符的部分算入答案。

4. UVA 11512 GATTACA

题目链接:https://onlinejudge.org/external/115/11512.html

求出现至少两次的最长子串,并统计其出现次数。最长长度就是

\[L=\max_{2\le i\le n}ht[i]. \]

\(L>0\) 时,连续满足 \(ht[i]\ge L\) 的一段对应一组拥有相同长度为 \(L\) 的前缀的后缀;若这一段含有 \(c\)\(ht\) 值,则对应 \(c+1\) 个后缀。该题可以练习如何从 Height 数组中的连续区间恢复重复次数。

5. 洛谷 P2852 [USACO06DEC] Milk Patterns G

题目链接:https://www.luogu.com.cn/problem/P2852

给定整数序列,求至少出现 \(K\) 次的最长连续子序列。先对数值离散化,再构造后缀数组。二分答案长度 \(L\),若存在连续 \(K-1\)\(ht\) 值均不小于 \(L\),就说明有连续 \(K\) 个后缀共享长度至少为 \(L\) 的前缀。

这题比前四题多出“整数离散化”和“二分答案”两步,但核心仍然只是观察 Height 数组中的连续段。

小结

倍增法的核心是:用长度为 \(w\) 的排名组成二元组,从而得到长度为 \(2w\) 的排名。再借助计数排序将每轮降至 \(O(n)\),最终在 \(O(n\log n)\) 时间、\(O(n)\) 空间内构造后缀数组。原模板不对第二关键字显式判界,而是结合 && 的短路求值与全局数组的零初始化表示空串;实际最多只会访问到下标 \(n+1\)

posted @ 2026-07-27 21:33  Meatherm  阅读(37)  评论(3)    收藏  举报