后缀数组 SA

没想到 SA 先写完。

算法介绍

前置:基数排序,计数排序,倍增。

P3809【模板】后缀排序

如果我们将字符串暴力存下来,然后暴力 sort,那么每次比较的复杂度为 \(O(n)\),时间复杂度为 \(O(n^2\log n)\)

当然,比较的这一部分时间是可以优化的,借助字符串哈希算法,可以在 \(O(\log n)\) 的时间内找到两个后缀第一个不一样的位置,根据字典序定义比较。总时间复杂度为 \(O(n\log^2 n)\)。因为本题的数据范围为 \(n\le 10^6\),显然没有通过的可能。由此引申出一种快速对后缀排序的算法。

大部分字符串算法将优化放在了利用已经求好的信息快速推出新的信息,后缀排序算法也是如此,我们记 \(sa_i\) 表示排名第 \(i\) 的后缀的开始位置,\(rk_i\) 表示以第 \(i\) 个位置开始的后缀的排名,不难发现 \(rk_i\)\(sa_i\) 是互逆运算,有 \(rk_{sa_i}=sa_{rk_i}=i\) 的性质,\(rk_i,sa_i\) 均是一个排列。

普通后缀排序算法的核心思想是倍增,设 \(f_{i,v}\) 表示仅比较后缀的前 \(2^v\) 位,后缀 \(i\) 的排名,则想要比较 \(i,j\) 两个后缀在前 \(2^{v+1}\) 位下的大小,则只需要以 \(f_{i,v},f_{j,v}\) 为第一关键字,\(f_{i+2^v,v},f_{j+2^v,v}\) 为第二关键字比较,直到 \(2^v>n\),此时比较结束。排序的部分可以用 sort 实现,且 \(f\) 第二维可以舍去。会倍增 \(\log n\) 次,每次排序复杂度为 \(O(n\log n)\),总时间复杂度为 \(O(n\log^2 n)\),仍然无法通过。

不过上面的算法仍然有了很大优化空间,两个字符串后缀的比较被映射到两个二元组之间的比较,且每个元素的范围都是 \(1\sim n\)。这不由得启发我们对排序进行优化,借助基于值域的线性排序方法,不难对只有一个关键字的元素序列排序,对于两个关键字的排序方法,需要借助基数排序的思想,先对第二关键字排序,再在不打乱相同元素顺序的前提下,对第一关键字排序。只需要排序两次就能完成,排序一次时间复杂度为 \(O(n)\),总时间复杂度为 \(O(n\log n)\)

还有一些细节,不详细展开。

m=max(n,300);//计数排序的值域,初始为 ACSLL 编码大小
memset(cnt,0,sizeof(cnt));//每次用之前都要清空
for(int i=1;i<=n;i++)cnt[rk[i]=s[i]]++;
for(int i=1;i<=m;i++)cnt[i]+=cnt[i-1];
for(int i=n;i>=1;i--)sa[cnt[s[i]]--]=i;//计数排序
for(w=1;w<n;w<<=1){
	memset(cnt,0,sizeof(cnt));
	for(int i=1;i<=n;i++)cnt[rk[i+w]]++;
	for(int i=1;i<=m;i++)cnt[i]+=cnt[i-1];
	for(int i=n;i>=1;i--)sa[cnt[rk[i+w]]--]=i;//第二关键字
	memset(cnt,0,sizeof(cnt));
	for(int i=1;i<=n;i++)cnt[rk[id[i]=sa[i]]]++;
	for(int i=1;i<=m;i++)cnt[i]+=cnt[i-1];
	for(int i=n;i>=1;i--)sa[cnt[rk[id[i]]]--]=id[i];//第一关键字
	for(int i=1;i<=n;i++)id[i]=rk[i];//要更新排名,需要拷贝一份
	for(int i=1,p=0;i<=n;i++){
    	if(id[sa[i]]!=id[sa[i-1]]||id[sa[i]+w]!=id[sa[i-1]+w])p++;//判断是否相同
    	rk[sa[i]]=p;
    }
}

当然,SA 存在线性求法,不过用途不大。这份代码也可以继续改进为常数更小版本。

LCP 相关

定义 \(\text{LCP}(u,v)\) 为后缀 \(u,v\) 的最长公共前缀,则记 \(height_i=\text{LCP}(sa_i,sa_{i-1})\)。这一节重点聚焦于 \(\text{LCP}\) 的相关理论问题。

结论们

  1. \(\forall i<j<k,\text{LCP}(sa_i,sa_k)=\min\{\text{LCP}(sa_i,sa_j),\text{LCP}(sa_j,sa_k)\}\)

不难证明(其实不大会)。

  1. \(\text{LCP}(i,j)=\min_{x\in (i,j]}\{height_x\}\)

根据结论一不断细分,最后带入 \(height\) 数组定义即可。

这个结论将两个后缀的 \(\text{LCP}\) 转化为区间最小值的问题,具有重要作用。

  1. \(\forall i>j>k,\text{LCP}(i,j)\ge \text{LCP}(i,k)\)

比较重要的单调性结论。浅浅证明一下。

\(d_1=\text{LCP}(i,j),d_2=\text{LCP}(i,k)\),若 \(d_2>d_1\),根据后缀排序定义,有后缀 \(k\) 的第 \(d_1+1\) 个字符等于后缀 \(i\) 的第 \(d_1+1\) 个字符(因为 \(d_1+1\le d_2\))。且这个字符大于后缀 \(j\) 的第 \(d_1+1\) 个字符(因为后缀 \(i,j\)\(d_1+1\) 个位置不等,但 \(i\)\(j\) 之后)所以推出 \(k>j\),与条件矛盾,故原命题的证。

\(height\) 数组

快速求 \(height\) 也是后缀排序的重要问题;记 \(h_i=height_{rk_i}\),则 \(h_i\ge h_{i-1}-1\) 成立,dalao 将之称为不完全单调性

考虑证明,当 \(h_{i-1}\le 1\) 时,结论显然成立。

否则记 \(u=i,v=rk_{sa_i-1},x=i-1,y=rk_{sa_{i-1}-1}\)。则有 \(\text{LCP}(x,y)=h_{i-1}>1\),考虑同时去掉 \(x,y\) 后缀的第一个字符,分别得到 \(u',v'\),根据后缀数组定义以及字符串中的位置关系,不难得出 \(u=u'>v'\),因为 \(v\)\(u\) 排名前一位的后缀,所以有 \(v\ge v'\),根据 \(\text{LCP}\) 的单调性,有 \(h_i=\text{LCP}(u,v)\ge\text{LCP}(u',v')=\text{LCP}(x,y)-1=h_{i-1}-1\),即可得出 \(h_i\ge h_{i-1}-1\)

考虑一下如何求,对于 \(rk_i=1\),特别定义此时的 \(h_i=0\)(因为没有前一项了),反之,对于第一个需要求的 \(h_i\),暴力求,复杂度 \(O(n)\),此后在上一个 \(h_i\) 减去一的基础上暴力推,注意到至多减去 \(n\) 次,所以至多推 \(n\) 次,因此求 \(height\) 的时间复杂度为 \(O(n)\)

for(int i=1,k=0;i<=n;i++){
	if(rk[i]==1)k=0;
	else{
		if(k>0)k--;
		int j=sa[rk[i]-1];
		while(i+k<=n&&j+k<=n&&s[i+k]==s[j+k])k++;
	}
	h[rk[i]]=k;
}

求出 \(height\) 后,可以利用数据结构快速维护最小值,来求出 \(\text{LCP}\),而利用 \(\text{LCP}\) 可以完成一些其他问题,如字符串匹配,有时甚至比 ACAM 等算法更具有优势。

例题详解

P4051 [JSOI2007] 字符加密

将串复制一份到串尾,然后对整个大串进行后缀排序,排序后将位置处于 \(1\sim n\) 的后缀拿出,取每个合法后缀第 \(n\) 个字符按顺序输出即可。

用于熟悉板子。时间复杂度 \(O(n\log n)\)

SP10419 POLISH - Polish Language

后缀排序后考虑动态规划,转移方程式为 \(f_i=\sum f_j[i<j,rk_i>rk_j]\),简单二维偏序,树状数组随便做,注意清空。

用来熟悉板子。时间复杂度 \(O(n\log n)\)

P2408 不同子串个数

经典问题,双倍经验 SP705

一个子串,可以看做某个后缀的前缀,若我们把每个后缀的所有前缀都当做一个独一无二的子串,数量即为 \(\frac{n(n+1)}{2}\),错的,因为可能存在一些后缀的前缀相同,具体来说,后缀 \(i,j\) 之间相同前缀的数量即为 \(\text{LCP}(i,j)\)

为了避免不重不漏,我们考虑计算排名 \(1\sim i\) 后缀算重复的部分,假设已经计算出排名 \(1\sim i-1\) 后缀计算重复的部分,则重点在于排名 \(i\) 的后缀有多少个前缀和排名 \(1\sim i-1\) 的后缀重复,根据式子即为 \(\max_{p\in [1,i-1]}\{\text{LCP}(sa_i,sa_k)\}\),而根据 \(\text{LCP}\) 的单调性,这个式子就是 \(height_i=\text{LCP}(sa_i,sa_{i-1})\)。所以最终答案即为 \(\frac{n(n+1)}{2}-\sum height_i\),时间复杂度 \(O(n\log n)\)

SP1811 LCS - Longest Common Substring

经典套路,将两个串拼在一起,串之间放一个诡异字符防止前后跨串匹配。答案即为最大的 \(\text{LCP}(i,j)\) 满足 \(i,j\) 分别属于不同的两个串。根据单调性,只需要分别取 \(sa_i,sa_{i-1}\) 即可,时间复杂度 \(O(n\log n)\)

P4248 [AHOI2013] 差异

前半部分好求,重点是后面 \(\sum\sum\text{LCP}(i,j)\) 有点难绷。

考虑 \(\text{LCP}(i,j)\) 其实是一个区间 \(height\) 的最小值,所以原问题转化为 \(height\) 序列所有子区间最小值之和,单调栈就可以,时间复杂度 \(O(n\log n)\)

P3181 [HAOI2016] 找相同字符

其实就是 \(\sum\sum\text{LCP}(i,j)\),照搬即可。

P2178 [NOI2015] 品酒大会

上强度了...吗?

若后缀 \(i,j\)\(r\) 相似的,则 \(\text{LCP}(i,j)\ge r\)。不难发现若后缀 \(i,j\)\(r\) 相似,则一定是 \(r-1\) 相似。考虑从大往小枚举 \(r\),类似扫描线推出答案。

先考虑方案数,后缀之间的 \(\text{LCP}\) 体现在区间最小值上,所以若两个后缀 \(r\) 相似,则他们排名的区间之中一定都是 \(height_i\ge r\) 的。若当前扫到 \(r\),将 \(height_i\ge r\) 的所有 \(i\),都让 \(sa_i\)\(sa_{i-1}\) 合并,每次合并计算新增的贡献,本质是个并查集,顺便维护一个 \(siz\) 数组即可。

考虑最大值,在维护并查集的时候搭配这将最大值,次大值维护,因为有负数,所以好需要维护最小值,次小值,会有位置不同但 \(a_i\) 相同的情况,所以要用用 multiset。每次合并后将 multiset 一起合并,合并后再更新答案。一种合并方法是只将一个集合的最大次大最小次小插入另一个集合,只需要插入 \(O(n)\) 次,太麻烦,要处理同一个值重复插入的问题。所以直接启发式合并,插入次数 \(O(n\log n)\)

总时间复杂度为 \(O(n\log^2 n)\),luogu 最大测试点运行不超过 0.6 s,LOJ 最大测试点运行不超过 0.4 s。record

P4094 [HEOI2016/TJOI2016] 字符串

私募样例。

直接下手太难,考虑二分答案 \(k\in [0,d-c+1]\),转化为判断是否存在一个后缀 \(i\in [a,b-k+1]\) 满足 \(\text{LCP}(i,c)\ge k\)

注意到 \(i\) 是不确定的,且 \(i\) 的限制在 \(\text{LCP}\) 里太过难做,考虑转化成像第一个条件一样的区间形式。

一个重点在于 \(\text{LCP}\) 具有单调性,\(\text{LCP}(x,a)\) 可以看做一个峰值在 \(a\) 取到的单峰函数。在 \(a\) 的左右两个二分出 \([L,R]\),满足任意 \(i\in [L,R]\) 都有 \(\text{LCP}(i,c)\ge k\)

这样两个限制一个是对下标的限制,一个是对 \(rk_i\) 的限制,本质是二维数点问题,考虑主席树,分别维护下标,\(rk_i\) 两个轴,单次查询 \(O(\log n)\)

总时间复杂度 \(O(q\log^2 n)\)。比较考察码力,record

P5284 [十二省联考 2019] 字符串问题

出生登场,2019 年省选何德何能同时聚集【字符串问题】,【骗分过样例】,【希望】这三位大神啊!!!

题面太抽象了,就是给你一个串 \(S\),给你指出 \(na\)\(A\) 子串 \([la_i,ra_i]\),指出 \(nb\)\(B\) 子串 \([lb_i,rb_i]\)\(m\)\(A\) 支配 \(B\) 的关系,要求你构造一个合法字符串满足如下要求:

  1. 字符串由是由 \(A\) 子串拼接形成的(不限子串使用次数)。

  2. 对于两个相邻 \(A\) 子串 \(S,T\),要存在一个 \(B\) 子串 \(P\),满足 \(S\) 能支配 \(P\)\(P\)\(T\) 的前缀。

好了,\(n,na,nb,|S|,m\) 都是 \(2\times 10^5\) 级别的。判断合法字符串可以构造的最长长度,若可以构造到无限长也需要当做特殊解输出。

考虑如何将这个问题具象化?支配关系不难想到有向边,\(A\to B\) 的支配关系已经给出,实际上 \(B\to A\) 的支配关系可以看成 \(B\)\(A\) 的前缀则 \(B\) 支配 \(A\)\(A,B\) 向他们各自支配的字符串连边,得到一张有向图。

无限长的情况就是有向图存在环,反之若构造的长度有限,则一定是一个 DAG。给 \(A\) 子串对应节点赋他们字符串本身的长度的权值,就是一个基本的 DAG 最长路问题,拓扑排序 dp 即可。

问题在于,\(A\to B\) 的边的数量在 \(O(m)\) 级别,但是 \(B\to A\) 的边是 \(O(n^2)\) 的,想要在这个思路走下去,必须要优化建图。

考虑 \(S[lb_i,rb_i]\)\(S[la_j,ra_j]\) 前缀的条件:

  1. \(rb_i-lb_i+1\le ra_j-la_j+1\):即 \(B\) 长度小于 \(A\)

  2. \(\text{LCP}(lb_i,la_j)\ge rb_i-lb_i+1\) :即两个后缀的 \(\text{LCP}\) 大于 \(|B|\)

根据上一问的套路,可以二分求出 \([L,R]\) 满足任意 \(p\in [L,R]\) 都有 \(\text{LCP}(p,lb_i)\ge |B|\)。第一个限制可以利用可持久化增加一维,\(rt_i\) 里插入了所有 \(|A|\ge i\)\(A\) 串的信息,只需要在 \(rt_{|B|}\) 里查找在 \([L,R]\)\(A\) 串即可。

具体的,将所有串按照长度从大到小一一插入主席树,串 \([la_j,ra_j]\) 插入到 \(rt_{|A|}\)\(rk_{la_j}\) 叶子的下方。插入完后枚举 \(B\) 串,借助主席树结构建图,一个串连不超过 \(O(n\log n)\) 条边,所以图的总边数不超过 \(O(n\log n)\) 级别。拓扑排序的复杂度为线性,所以总的时间复杂度就是 \(O(n\log n+m)\),常数较大,不过即使 vector 存图也能稳定通过。record

The End

学了 SA 之后才发现貌似 SAM 很好用。

不过最近不会学了,顶多再水两道题。

posted @ 2025-08-10 21:16  zuoqingyuan111  阅读(13)  评论(0)    收藏  举报