后缀数组(SA)学习笔记

终于开始补省冬的锅了/kk

学习笔记参考xMinh dalao 的博客

一、后缀数组的相关定义

1.子串:在字符串\(s\)中,取任意\(i \le j\),那么在\(s\)中截取\(i\)\(j\) 的这一段就叫做\(s\)的一个子串

2.前缀:\(s[1...i]\)\(1 \le i \le n\)

3.后缀:\(s[i...n]\)\(1 \le i \le n\)

后缀数组:
对于一个字符串\(s\)的后缀按照字典序排序的结果。

\(suff(i)\)\(s[i...n]\)

\(sa_i\)表示排名为i的后缀的起始位置\(rank_i\)表示从i开始的后缀的排名,也就是说\(rank_{sa_i}\)=\(i\)

二、求\(sa_i\)的方法——倍增

复杂度\(O(n log n)\)

暴力复杂度\(O(n^2 log n)\)

  • 读入字符串后进行排序(按照每个后缀的第一个字符排序)。

  • 对于每一个字符,我们按照字典序给一个排名,这里也叫关键字

如图

  • 此时再把相邻两个关键字合并,以第一个字母的排名为第一关键字,第二个字母的排名为第二关键字,没有第二关键字的设为\(0\)

如图

  • 注意到现在第\(i\)位上的关键字为 \(suff(i)\)的前两个字符的排名 ,而第\(i+2\)位上的关键字为 \(suff(i+2)\)的前两个字符的排名 ,所以合并起来就是 \(suff(i)\)的前四个字符的排名

  • 这就运用到倍增的思想

  • 显然,当所有排名都不同的时候就可以退出啦!

  • 时间复杂度稳定在 \(O(log n)\)

三、基数排序(桶排序)

用快排的话是\(O(nlog^2n)\)的,注意到每次排序都是排两个数的,所以设两个桶\(x\) , \(y\) ,一个存第一关键字,一个存第二关键字,每次排序的复杂度为\(O(n)\)

优化后的复杂度为\(O(nlogn)\)

Code:

#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;
const int maxn=1e6+5;
char s[maxn];
int sa[maxn],x[maxn],y[maxn],rank[maxn],c[maxn];
int n,m;
//sa[i]表示排名为i的后缀的起始位置,rank[i]表示从i开始的后缀的排名,也就是说sa[i]和rank[i]反过来的
//x[i],y[i]分别为第i个元素的第1关键字和第2关键字
//c[i]为桶 
inline void SA(){
	for(int i=1;i<=n;i++) ++c[x[i]=s[i]];
	for(int i=2;i<=m;i++) c[i]+=c[i-1];//得出每个关键字最多在第几名 
	for(int i=n;i>=1;i--) sa[c[x[i]]--]=i;
	for(int k=1;k<=n;k<<=1){//倍增 
		int num=0;
		for(int i=n-k+1;i<=n;i++) y[++num]=i;//显然,第n-k+1~n位无第二关键字
		for(int i=1;i<=n;i++) 
			if(sa[i]>k) y[++num]=sa[i]-k;
		//排名为i的数 在数组中是否在第k位以后
        //如果满足(sa[i]>k) 那么它可以作为别人的第二关键字,就把它的第一关键字的位置添加进y就行了
        //所以i枚举的是第二关键字的排名,第二关键字靠前的先入队  
        for(int i=1;i<=m;i++) c[i]=0;//初始化 
        for(int i=1;i<=n;i++) ++c[x[i]];
        for(int i=2;i<=m;i++) c[i]+=c[i-1];
        for(int i=n;i>=1;i--) sa[c[x[y[i]]]--]=y[i],y[i]=0;
        //因为y的顺序是按照第二关键字的顺序来排的 
        //第二关键字越靠后的,在同一个第一关键字桶中排名越靠后 
        //基数排序 
        swap(x,y);
		num=1;x[sa[1]]=1;
		for(int i=2;i<=n;i++)
			x[sa[i]]=(y[sa[i]]==y[sa[i-1]]&&y[sa[i]+k]==y[sa[i-1]+k])?num:++num;
		if(num==n) break;
		m=num;
	}
	for(int i=1;i<=n;i++) printf("%d ",sa[i]);
	return;
}
int main(){
	scanf("%s",s+1);
	n=strlen(s+1);m=122;//'z'的ASCLL码是122
	SA();
	return 0;
}
/*
ababa

5 3 1 4 2
*/

四、后缀数组的辅助工具——最长公共前缀\((LCP)\)

  1. 定义:\(LCP(i,j)\)\(suff(i)\)\(suff(j)\)的最长公共前缀。

  2. 显然:

\(LCP(i,j)\)=\(LCP(j,i)\)

\(LCP(i,i)=n-sa_i+1\)

  1. 性质:

\(LCP(i,k)=min(LCP(i,j),LCP(j,k))\) \((1 \le i \le j \le k \le n)\)

\(LCP(i,k)=min(LCP(j,j-1))\)\((1 \le i \le j \le k \le n)\)

懒得证明了hhh

重点!那么如何求LCP呢

定义\(height_i\)\(LCP(i,i-1)\)

特别的\(height_1=0\)

最关键的一条性质:\(height[rank_i]>=height[rank_{i-1}]-1\)

证明:

\(height[rank[i-1]]=0\)时显然

否则设\(u=sa[rank[i-1]-1],v=sa[rank[i-1]]=i-1\)

必有\(s[u]=s[v]\)

由于\(s_u\)的排名小于\(s_v\),所以\(s_{u+1}\)的排名必然小于\(s_v\)

所以必然存在一个排名小于\(s_u\)的后缀,与\(suff(sa_i)\)的LCP长度\(>=height[rank[i-1]]-1\)

被自己绕晕了

所以我们拥有了一个\(O(n)\)\(height\)数组的优秀做法~~~

按照\(rank_1 rank_2 ... rank_n\)的顺序求

\(k=height_{rank_i}\)

求完\(rank_{i-1}\)\(rank_i\),如果\(k>0\)\(k--\)

检查\(s[i+k]\)是否等于\(s[rank[i-1]+k]\),如果是就\(k++\)

最后 \(height_{rank_i}=k\)

Code

inline void LCP(){
	int k=0;
	for(int i=1;i<=n;i++) rank[sa[i]]=i;
	for(int i=1;i<=n;i++){
		if(rank[i]==1) continue;
		if(k) k--;//h[i]>=h[i-1]+1;
		int j=sa[rank[i]-1];
		while(i+k<=n&&j+k<=n&&s[i+k]==s[j+k]) k++;
		h[rank[i]]=k;
	}
	for(int i=1;i<=n;i++) printf("%d ",h[i]);
	return ;
}

所以\(LCP(i,j)\)\(height\)数组中第\(rank_i+1\)到第\(rank_j\)个数的最小值,可以\(O(nlogn)\)预处理RMQ,\(O(1)\)回答


应用:求一个串\(s\)本质不同的子串个数

一个串的子串可以表示为这个串的一个后缀的前缀

所以本质不同的串的个数相当于所有后缀的集合中,本质不同的前缀的个数

加入后缀\(s[sa[i]...n]\)后会产生\(n-sa_i+1\)个子串

显然子串中所有长度\(\le height_i\)的子串都已经在前面出现过了 (\(height_i\)可以表示为比\(sa_i\)小的后缀与\(sa_i\)的LCP最大值)

所以本质不同的串的个数为

\(\sum\limits_{i=1}^{n}{(n-sa_i+1-height_i)}\)\(=\left(\dfrac {n*(n+1)} 2\right)-\)\(\sum\limits_{i=2}^{n}height_i\)

posted @ 2020-04-29 12:48  Ciciiiii  阅读(334)  评论(0)    收藏  举报