• 后缀数组详解


    后缀数组炒鸡简单的

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

    〇、朴素解法

    对字符串后缀进行快排,(O(N^2 log N))

    一、字符串哈希

    将字符串的所有后缀进行哈希,所有后缀的哈希值珂以在(O(N))的时间求出

    然后就珂以(O(1))比较字符串是否相等

    这样,我们就得到了一个改进算法:比较两个后缀时,二分它们的 LCP(最长公共前缀) 长度,并比较第一位不同的字符,总时间复杂度为(O(N log^2 N))

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

    二、倍增法

    上述两个算法,我们都是将两个后缀看做独立字符串进行比较,而忽视了后缀之间的内在联系。一个更优的算法是倍增算法,它的主要思路是,每次利用上一次的结果,倍增计算出从每个位置 (i) 开始长度为 (2^k) 的子串的排名

    如果使用快速排序来实现双关键字排序,总时间复杂度为 (O(N log^2 N)),实现难度比 Hash 的方法要低,并且更稳定。而考虑到每个关键字均为 ([−1,N)) 的整数,我们可以使用 (O(N)) 的基数排序,来将总时间复杂度将为 (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]
  • 相关阅读:
    SOJ 2785_Binary Partitions
    Codeforces Round #328 (Div. 2)
    C++ fill 和memset
    SOJ 2749_The Fewest Coins
    Codeforces Round #327 (Div. 2)
    TYVJ P1013 找啊找啊找GF Label:动态规划
    TYVJ 1014 乘法游戏
    TYVJ 1011 NOIP 2008&&NOIP 2000 传纸条&&方格取数 Label:多线程dp
    错误集合
    TYVJ P1038/P1039 忠诚 标签:线段树
  • 原文地址:https://www.cnblogs.com/yzhang-rp-inf/p/10170884.html
Copyright © 2020-2023  润新知