后缀数组详解

后缀数组详解

后缀数组炒鸡简单的

以下算法都以Luogu P3809 【模板】后缀排序为例

〇、朴素解法

(O(N^2 log N))

一、字符串哈希

(O(N))的时间求出

(O(1))比较字符串是否相等

(O(N log^2 N))

使用 Hash 来构造后缀数组的好处在于时间复杂度较低,并且珂以动态维护(珂以使用set),坏处在于 Hash 的不稳定性。

二、倍增法

(2^k) 的子串的排名

(O(N logN))

完整代码

#include <bits/stdc++.h>
#define N 1000005
using namespace std;
inline void write(register int x)
{
    if(!x)putchar('0');if(x<0)x=-x,putchar('-');
    static int sta[20];register int tot=0;
    while(x)sta[tot++]=x%10,x/=10;
    while(tot)putchar(sta[--tot]+48);
}
char s[N];
int n,m;
int rak[N],tp[N],sa[N],tax[N];
inline void Qsort()
{
    for(register int i=1;i<=m;++i)
        tax[i]=0;
    for(register int i=1;i<=n;++i)
        ++tax[rak[i]];
    for(register int i=1;i<=m;++i)
        tax[i]+=tax[i-1];
    for(register int i=n;i>=1;--i)
        sa[tax[rak[tp[i]]]--]=tp[i];
}
inline void suffixsort()
{
    m=80;
    for(register int i=1;i<=n;++i)
        rak[i]=s[i]-'0',tp[i]=i;
    Qsort();
    for(register int w=1,p=0;p<n;m=p,w<<=1)
    {
        p=0;
        for(register int i=1;i<=w;++i)
            tp[++p]=n-w+i;
        for(register int i=1;i<=n;++i)
            if(sa[i]>w)
                tp[++p]=sa[i]-w;
        Qsort();
        swap(tp,rak);
        rak[sa[1]]=p=1;
        for(register int i=2;i<=n;++i)
            rak[sa[i]]=(tp[sa[i-1]]==tp[sa[i]]&&tp[sa[i-1]+w]==tp[sa[i]+w])?p:++p;
    }
    for(register int i=1;i<=n;++i)
        write(sa[i]),putchar(' ');
}
int main()
{
    scanf("%s",s+1);
    n=strlen(s+1);
    suffixsort();
    return 0;
}

但是这仍然在较大数据范围时比较吃力(毒瘤)

三、DC3

常数巨大

①先将后缀分成两部分,然后对第一部分的后缀排序;

②利用①的结果,对第二部分的后缀排序;

③将①和②的结果合并,即完成对所有后缀排序;

(O(N)),但常数极大,在较小数据范围时与倍增算法相比无明显优势,且实现复杂,性价比较低

比倍增法跑的还慢的dc3

#include<bits/stdc++.h>
#define N 1000005
using namespace std; 
inline void write(register int x)
{
    if(!x)putchar('0');if(x<0)x=-x,putchar('-');
    static int sta[20];register int tot=0;
    while(x)sta[tot++]=x%10,x/=10;
    while(tot)putchar(sta[--tot]+48);
}
char s[N];
int str[N*3],sa[N*3],rank[N],height[N];
int id[N];
inline bool equal(register int *str,register int a,register int b)
{
	return str[a]==str[b]&&str[a+1]==str[b+1]&&str[a+2]==str[b+2];
}
inline bool cmp3(register int *str,register int *nstr,register int a,register int b)
{
	if(str[a]!=str[b])
		return str[a]<str[b];
	if(str[a+1]!=str[b+1])
		return str[a+1]<str[b+1];
	return nstr[a+b%3]<nstr[b+b%3];
}
inline void Qsort(register int *str,register int *sa,register int *res,register int n,register int m)
{
	for(register int i=0;i<m;++i)
		id[i]=0;
	for(register int i=0;i<n;++i)
		++id[str[sa[i]]];
	for(register int i=0;i<m;++i)
		id[i+1]+=id[i];
	for(register int i=n-1;i>=0;--i)
		res[--id[str[sa[i]]]]=sa[i];
}
inline void dc3(register int *str,register int *sa,register int n,register int m)
{
	#define F(x) ((x)/3+((x)%3==1?0:one))
	#define G(x) ((x)<one?(x)*3+1:((x)-one)*3+2)
	int *nstr=str+n,*nsa=sa+n,*tmpa=rank,*tmpb=height;
	int len=0,num=0,zero=0,one=(n+1)/3;
	for(register int i=0;i<n;++i)
		if(i%3)
			tmpa[len++]=i;
	str[n]=str[n+1]=0;
	Qsort(str+2,tmpa,tmpb,len,m);
	Qsort(str+1,tmpb,tmpa,len,m);
	Qsort(str+0,tmpa,tmpb,len,m);
	nstr[F(tmpb[0])]=num++;
	for(register int i=1;i<=len-1;++i)
		nstr[F(tmpb[i])]=equal(str,tmpb[i-1],tmpb[i])?num-1:num++;
	if(num<len)
		dc3(nstr,nsa,len,num);
	else 
		for(register int i=0;i<len;++i)
			nsa[nstr[i]]=i;
	if(n%3==1)
		tmpa[zero++]=n-1;
	for(register int i=0;i<len;++i)
		if(nsa[i]<one)
			tmpa[zero++]=nsa[i]*3;
	Qsort(str,tmpa,tmpb,zero,m);
	for(register int i=0;i<len;++i)
		tmpa[nsa[i]=G(nsa[i])]=i;
	for(register int i=0,j=0,k=0;k<n;++k)
		if(j>=len||(i<zero&&cmp3(str,tmpa,tmpb[i],nsa[j])))
			sa[k]=tmpb[i++];
		else 
			sa[k]=nsa[j++];
}
int main()
{
	scanf("%s",s);
	int n=strlen(s);
	str[n]=0;
	for(register int i=0;i<n;++i)
		str[i]=s[i];
	dc3(str,sa,n+1,256);
	for(register int i=1;i<=n;++i)
		write(sa[i]+1),putchar(' ');
	return 0;
}

四、SA-IS

待填坑

五、后缀数组的一些应用

所有操作都基于Height数组

Height[i]=lcp(sa[i],sa[i-1]),即排名为i的后缀与排名为i−1的后缀的最长公共前缀

求法:

inline void GetHeight() 
{
    int k=0;
    for(register int i=1;i<=n;++i)
    {
        if(k) 
			--k;
        int j=sa[rak[i]-1];
        while(s[i+k]==s[j+k])
			++k;
        Height[rak[i]]=k;
    }
}

rak:从第i个位置开始的后缀的排名

rak珂以根据rak[sa[i]]=i,sa[rak[i]]=i快速求出

有了Height数组后就珂以胡作非为

1.两个后缀的最大公共前缀

lcp(x,y)=min(Height[x~y]),用rmq维护,O(1)查询

2.可重叠最长重复子串

Height数组里的最大值

3.不可重叠最长重复子串

首先二分答案x,对Height数组进行分组,保证每一组的min_Height都>=x
依次枚举每一组,记录下最大和最小长度,当sa[maxlen]−sa[minlen]>=x那么可以更新答案

4.本质不同的子串的数量

枚举每一个后缀,第i个后缀对答案的贡献为len−sa[i]+1−Height[i]