[学习笔记]后缀数组

[学习笔记]后缀数组

零.前言与一些规定

​ 整了两三天,感觉自己把 09 年的那篇论文看懂了一点,题倒是没做几道,打算写一下以防忘记。

​ 在此有以下定义:

  • 字符串 S 为 \(s[1...n]\) ,其中后缀 i 指 \(s[i...n]\)
  • \(sa[i]:\) 排名为 i 的后缀在原字符串的位置
  • \(rank[i](rk[i]):\) 后缀 i 的排名
  • \(sa,rk\) 形成一个一一对应关系,即 \(sa[rk[i]]=rk[sa[i]]=i\)
  • \(lcp(i,j)\) 后缀 i 和后缀 j 的最长公共前缀
  • \(height[i](ht[i]):\) \(lcp(sa[i],sa[i-1])\)

一.求解 sa 与 rk

​ 因为有"排名"的概念,所以我们首先应该明确的是"排序"的规则。此处的排序并不等于一般意义上的排序,而是以第一个不相同的字符(若是没有则补空)的相对字典序大小决定。

​ 那么我们能想到一个十分朴素的算法,即将每一个后缀列出来,然后直接排序,但是这个算法显然很不行,于是考虑倍增算法。

​ 首先要知道什么是计数排序和基数排序。如果不知道建议先去了解一下,很简单的。然后我们设 \(rk_w[i]\) 为在所有只计算前 \(w\) 个字符的后缀中的排名,换而言之,每一个后缀的长度被限制到了 \(w\) ,然后考虑如何去求出 \(rk_{2w}[i]\) 。很好想的,只需要\(rk_w[i+w]\) 作为第二关键字,\(rk_w[i]\) 作为第一关键字,跑一遍基数排序就能得到新的顺序。

​ 然后这里有几个小细节。

​ 第一,若是长度不够,则以空(最小值补齐),所以数组应当开两倍。

​ 第二,虽然最后的所有后缀肯定互不相同,但是在中间是极有可能出现相同的,出于一些非常必要且特殊的原因(比如减小复杂度),中途的\(rk_w\) 需要去重,或者说是同样的字符串对应的 \(rk\) 须得是相同的。并且一开始为了方便 \(w=1\) 的时候 \(rk_1[]\) 是并不连续的,或者说并不严格,会出现“断层”,意思就是会有 \(rk_1[a]=1,rk_2[c]=3\) 但是并没有 \(b\) 这个元素的情况。但是在之后的倍增中就会严格。

​ 第三,最后的终止条件是 \(w>n\) ,十分显然,因为比较的原理是没有就补空。

​ 第四,基数排序中,第二关键字的排序并不需要运用到计数排序,因为这一次的第二关键字也是上一次的排序结果,是有序的,直接利用就行,代码中有着十分良好的体现。

​ 第五,我们始终希望原本位置相对靠后的,虽然相同,但是对应到它的 \(sa\) 会相对大一些。

CODE

using namespace std;
#define fe(i,a,b) for(int i=a;i<=b;++i)
#define ef(i,b,a) for(int i=b;i>=a;--i)
const int MAXN=2e6+5;
char s[MAXN];
int rk[MAXN],id[MAXN],oldrk[MAXN],cnt[MAXN],sa[MAXN],n,m;
inline bool cmp_SA(int i,int w){
	return oldrk[sa[i]]==oldrk[sa[i-1]]&&oldrk[sa[i]+w]==oldrk[sa[i-1]+w];//相同的充要条件
}
int main(){
	scanf("%s",s+1);
	n=strlen(s+1),m=300;
	fe(i,1,n)++cnt[rk[i]=s[i]];//一开始的rk并不很严格,第二点
	fe(i,1,m)cnt[i]+=cnt[i-1];
	ef(i,n,1)sa[cnt[rk[i]]--]=i;//相对靠后,第五点
	for(int w=1,p=0;w<n;w<<=1,m=p,p=0){//p用来优化值域
		ef(i,n,n-w+1)id[++p]=i;//先将第二关键字含有无限小(空)的放在前面,这里的循环顺序没有关系
		fe(i,1,n)if(sa[i]>w)id[++p]=sa[i]-w;//第二关键字重复利用,第四点
		memset(cnt,0,sizeof(cnt));
		fe(i,1,n)++cnt[rk[id[i]]];
		fe(i,1,m)cnt[i]+=cnt[i-1];
		ef(i,n,1)sa[cnt[rk[id[i]]]--]=id[i];//计数排序与相对靠后
		memcpy(oldrk,rk,sizeof(rk));
		p=0;
		fe(i,1,n)rk[sa[i]]=cmp_SA(i,w)?p:++p;//重新计算值域
	}
	fe(i,1,n)printf("%d ",sa[i]);
	return 0;
}

二.Height 数组

​ 知道了前面两个基础数组之后,接下来是最难懂但是最有用的 \(Height\) 数组了。从其定义可以看出 \(ht[i]=lcp(sa[i],sa[i-1])\),即第 i 小的后缀和 i-1 小之间的最长公共前缀。然后又去思考“排序”排出来的意义,实际上就是将前缀差异最小的放在一起,忽略了开头在原串中的位置,差异越小越近,而 \(ht\) 反映了这个差异有多小,或者说有多么相似,重复的开头有多么长。

​ 所以可以写出一个定义式。

\[height[i]=lcp(sa[i],sa[i-1])=max\{lcp(sa[i],j),rank[j]<rank[i]\} \]

1.ht的性质

​ 多用于求 \(lcp(i,j)\),式子为 \(lcp(i,j)=min\{height[rank[i]+1...rank[j]]\},rank[i+1]\leq rank[j]\),或者是带入恒等式为 \(lcp(sa[i],sa[j])=min\{height[i+1...j]\}\),可以很直观的感受到它的意义,由于排序相隔最近的总是差异最小,可以理解为用最小的变化去靠近,或者说是这个最小值代表了这么长的前缀一直重复没有变过,可以手玩体会。

​ 由于子串相当于是某一个后缀的前缀,所以多用于解决子串的出现与大小问题。

2.ht的求解及其引理

​ 首先我们有一个及其重要的引理

\[height[rank[i]]\geq height[rank[i-1]]-1 \]

我大概会口胡证明:

​ 首先明确以下几个显而易见的结论:

  • 在空串也视为后缀的意义下,所有的非空后缀删去第一个字符都是一个合法的后缀

  • 后缀开头位置的+1等同于删去一个字符,-1等同于在前面补充一个合法字符

  • \(lcp(i,j)=k \Rightarrow lcp(i+1,j+1)=k-1(k>0)\)(1)

  • 若有 \(lcp(i,j)=k,rank[i]<rank[j]\),则 \(\forall x\in[0,k],rank[i+x]<rank[j+x]\)(2)

    接下来开始证明,记后缀 i 为 \(A\) ,后缀 \(i-1\)\(aA\) ,那么当 \(ht[rk[i-1]]=0\) 时,式子显然成立,不等于 0 时,有 \(lcp(sa[rk[i-1]],sa[rk[i-1]-1])=k > 0\),带入恒等式 \(lcp(i-1,sa[rk[i-1]-1])=k>0\),于是使用结论(1) \(lcp(i,sa[rk[i-1]-1]+1)=k-1\),

    可以知道 \(sa[rk[i-1]-1]+1\) 是一个合法的后缀,换句话说,存在一个合法的后缀 \(j=sa[rk[i-1]-1]+1\) 使得 \(lcp(i,j)=k-1=height[rank[i-1]]-1\) ,由 \(rk[i-1]-1<rank[i-1]\) 使用结论(2)知道 $rank[j]<rank[i] $然后由于定义式,得证。

    此处也可以用 \(lcp\) 的求解来理解,\(k-1\)\(min\{height[rank[j]+1...rank[i]]\}\)

这个引理近似于一个单调不降的性质,于是可以非常暴力的求解 height 数组,给出代码。

CODE

for(int p=0,i=1;i<=n;++i){
	if(p)p--;
	while(s[sa[rk[i]-1]+p]==s[sa[rk[i]]+p])++p;//这里也可以写成 s[i+p]
	ht[rk[i]]=p;
}

三.常用范围

0.序

主要是照搬09年的那篇论文,以后做了题会补充吧\cy

1. 一个字符串

最长重复子串(可重叠)

​ 等价于两个后缀的最长公共前缀,有很多种方式可以证明一定是来源于 \(height\) 数组,且为其中的最大值。

最长重复子串(不可重叠)

​ 由于很明显满足单调性,先二分长度,然后将连续的大于长度 \(height\) 分组,看其中 \(sa\) 的最大最小值之差是否大于长度。

出现 k 次的最长子串(可重叠)

​ 二分长度,分组。看组内的size是否大于等于 k。

​ 或者在 height 上面整 size 为 k-1 的滑动窗口,取最小值的最大值。

不相同的子串个数

​ 对于每一个后缀 i ,会产生 \(height[i]\) 个重复的前缀,实际上所有的重复为 \(lcp(i,k)\) ,但是两两对应和不重复相减,取一个最大的 \(lcp(i,k)\) ,k的rank在他之前 ,即为 height。

​ 整合就是 \(Ans=\sum n-sa[i]+1-height[i]=\sum i-height[i] =\frac{n*(n+1)}{2}-\sum height[i]\)

回文子串

​ 将字符串反过来拼在后面,中间用一个特殊字符隔开,然后找对应的两个后缀的 \(lcp\) ,时间复杂度在 \(nlogn\) (用DC3和RMQ结合可以做到 \(O(n)\) 但是我不会hhhhh)

连续重复子串

S 由一个字符串 k 重复而成。枚举长度看 \(lcp(1,len+1)\) 是否等于 \(n-k\) 即可,预处理一手做到 \(O(n)\)

连续重复出现次数最多的子串

枚举长度。重复若干次肯定包含了重复两次,也就是说在\(s[1],s[1+L],s[1+2*L]...s[1+k*L]\) 中肯定有两个位置相同,从这两个位置向左右匹配,看最长的 \(lcp\),然后稍加计算即可。

2.两个字符串

最长公共子串

暴力拼在一起,中间隔开。然后看相邻 sa 所属不同的 height 的最大值。

可以简单证明答案一定在 height 处产生,因为原题相当于求一个最大的 \(lcp(i,j)\) ,那么 \(rank[i]...rank[j]\) 之间肯定有一个相邻的 sa 相异,否则不产生答案。且这个 \(lcp\) 一定小于等于那个位置的 height

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

拼接。分组。在每个组内对于 sa 相异的组合数学算一算。

3.多个字符串

出现在不小于 k 个字符串中的最长公共子串

二分长度。分组。看每组来自几个不同的字符串。
[upd21.2.17] 疑似可以尺取然后写一个伪单调对列做到线性,不过瓶颈卡在构造SA上了哈哈哈哈

每个字符串中出现若干次且不重叠的最长子串

二分长度。分组。看来源于相同字符串的 sa 最大最小值之差。

在每个字符串中出现或者反转后出现

先将所有字符串反写。所有正反拼接。二分长度,分组。组内判断是否在对应的两个字符串中任一出现。

posted @ 2021-02-02 11:52  clockwhite  阅读(111)  评论(0编辑  收藏  举报