• 后缀数组SA学习笔记


    什么是后缀数组

    后缀数组(sa[i])表示字符串中字典序排名为(i)的后缀位置
    (rk[i])表示字符串中第(i)个后缀的字典序排名
    举个例子:

    ababa
         a b a b a
    rk:3 5 2 4 1
    sa: 5(a) 3(aba) 1(ababa) 4(ba) 2(baba)
    

    那么就有(sa[rk[i]]=rk[sa[i]]=i)

    后缀数组的求法

    二周目

    倍增法

    看一会儿还是比较好记的
    但没有理解每句话是在干什么的话以后再写就会没有思路
    因此这里简述一下基本过程和一些关键细节
    之后使用后缀(i)表示位置为(i)的后缀,第(i)个后缀表示排名为(i)的后缀,特此说明

    倍增法的基本过程:

    首先将所有后缀的第一个字符拿出来做一次排序,并记录其排名,字母相同的排名相同
    第二步,将所有后缀的第一个字母和第二个字母拿出来做排序;
    我们知道后缀(i)的第二个字母的排名就是后缀(i+1)的排名,后缀(n)没有第二个字母,因此其排名为(0)
    此时对于((rk[i],rk[i+1]))的二元组进行第二次排序,并重新记录其排名
    之后求出包括前四个字母((rk[i],rk[i+2]))的排名,包括前(2p)个字母((rk[i],rk[i+p]))的排名...直到(pge n)为止。
    如果直接按照上面的方法模拟,复杂度将是(O(nlog^2n))

    关键细节:基数排序

    其实直接模拟用快排求后缀数组思想比较简单
    但是基数排序求后缀数组代码简单并且复杂度为(O(nlogn))较为优秀
    所以现在一般使用的算法都是基数排序。

    我们的基数排序到底是个什么东西呢?
    假设我们现在要对一些二元组((a,b))进行排序
    (x)表示每个元素对应的a离散之后的权值
    (y)表示将每个元素按照b排序后的元素编号,(y)数组为一个(1-n)的排列
    (z)表示将每个元素按照(a,b)排序后的编号,
    注意由于(y)数组的存在,求出的(z)的排名一定是互不相同的
    (t)表示桶数组
    则代码如下:

    int n;
    int x[N],y[N],t[N],z[N];
    inline void Rsort(){
        for(int i=1;i<=n;i++)t[x[i]]++;
        for(int i=1;i<=n;i++)t[i]+=t[i-1];
        for(int i=n;i;i--)z[t[x[y[i]]]--]=y[i];
    }
    

    第一句话就是把所有元素的第一维全部丢到桶里去
    第二句话就是把所有的桶前缀和,前缀和之后(t[i])表示的是第一维(a)权值(le i)的元素个数
    前两句话都容易理解。
    但是第三句话是个什么东西?那么多的数组调用是怎么回事?
    别急,我们慢慢分析。
    求出了(t[i])之后,我们怎么求出元素现在的排名啊?
    因为我们的(t[i])表示的是第一维(a)权值(le i)的元素个数
    那么如果第一维(a)权值为(i)的元素的个数为(c_i)个,
    那么权值为(i)的元素的排名区间就是([t[i-1]+1=t[i]-c[i],t[i]])
    而我们的只要对于每一个(i)(t[x[i]]--)就可以保证结果的第一维(a)一定是符合要求的
    像这样

        for(int i=1;i<=n;i--)z[t[x[i]]]=i,t[x[i]]--;
    

        for(int i=1;i<=n;i--)z[t[x[i]]]=i,t[x[i]]--;
    

    但这样无法满足第二维符合要求,怎么办呢?
    我们发现上面的代码中(i)的枚举顺序是可以改变而不影响第一维(a)排序后符合要求这个条件的
    于是我们现在就是要找到一种枚举(i)的顺序,使得第二维(b)排序后也符合要求
    我们知道在权值为(i)的所有元素中第一个使(t[i])自减的元素得到的排序位置是最大的
    所以我们按照倒序枚举(i)第二维的编号排名(y[i]),

        for(int i=n;i;i--)//change i to y[i]
    

    这样第一个权值为(i)的所有元素中第一个使(t[i])自减的元素的第二维肯定是最大的。
    所以这样排序就符合要求啦

        for(int i=n;i;i--)z[t[x[y[i]]]--]=y[i];
    

    主体部分

    这里都是一些细节类的东西,我就直接放在注释里啦

    inline void getsa(){
        for(int i=1;i<=n;i++)y[i]=i,t[x[i]=a[i]]++;Rsort();
        for(int k=1,p;k<=n;k<<=1){
            p=0;
            for(int i=n-k+1;i<=n;i++)y[++p]=i;
            //最后k位是没有权值的,并且已经排好序,因此第二维的值为0,需要放在最前面
            for(int i=1;i<=n;i++)if(sa[i]>k)y[++p]=sa[i]-k;
            //这里按照后缀的排名进行正序枚举,枚举第二维可能出现的后缀位置,如果$sa[i]>k$证明这个后缀会后缀$sa[i]-k$作为二元组,它在第二维中的排名肯定是所有未枚举中最靠前的
            Rsort();
            swap(x,y);
            //此时y变成了x,即原序号;
            //我们应当记得我们是要对(x[i],x[i+k])的二元组进行排序
            x[sa[1]]=p=1;
            for(int i=2,p0,p1;i<=n;i++){
                p0=sa[i-1];p1=sa[i];
                x[p1]=(y[p0]==)?p:++p;
            }
            //仍然正序枚举后缀的排名,离散化
            //枚举sa[i]其实就是枚举基数排序后的第i个元素
        }
    }
    

    完整代码

    int n,m,sa[N],x[N],y[N],t[N];
    char s[N];
    il bool cmp(int i,int j,int k){return y[i]==y[j]&&y[i+k]==y[j+k];}
    il void getsa(){
        m=200;for(RG int i=1;i<=n;i++)t[x[i]=(s[i]-'0')]++;
        for(RG int i=1;i<=m;i++)t[i]+=t[i-1];
        for(RG int i=n;i;i--)sa[t[x[i]]--]=i;
        for(RG int k=1,p;k<=n;k<<=1){
            p=0;
            for(RG int i=0;i<=m;i++)t[i]=y[i]=0;
            for(RG int i=n-k+1;i<=n;i++)y[++p]=i;
            for(RG int i=1;i<=n;i++)if(sa[i]>k)y[++p]=sa[i]-k;
            for(RG int i=1;i<=n;i++)t[x[y[i]]]++;
            for(RG int i=1;i<=m;i++)t[i]+=t[i-1];
            for(RG int i=n;i;i--)sa[t[x[y[i]]]--]=y[i];
            swap(x,y);x[sa[1]]=p=1;
            for(RG int i=2;i<=n;i++)x[sa[i]]=(cmp(sa[i],sa[i-1],k)?p:++p);
            if(p>=n)break;m=p;
        }
    }
    

    后缀数组的一些性质

    摘自:
    http://hihocoder.com/problemset/problem/1403,
    http://hihocoder.com/problemset/problem/1407,
    http://hihocoder.com/problemset/problem/1415,
    http://hihocoder.com/problemset/problem/1419,
    有删改

    Height数组

    定义(Height)数组表示排名为((i-1))的后缀和排名为(i)的后缀的最长公共前缀(Longest Common Prefix,LCP)。
    (Height[i]=LCP(i-1,i))

    1:若(rank_j<rank_k),则有(LCP(j,k)=min_{i=1}^{rank[k]-rank[j]}Height[rank[j]+i])
    这个性质显然。
    因此,我们可以使用(ST)表在(O(nlogn)+O(1))的时间内快速查询两个后缀的(LCP)

    int S[21][N],p[N],lg[N],ans;
    il void init(){
    	p[0]=1;for(RG int i=1;p[i-1]<=n;i++)p[i]=p[i-1]*2;
    	for(RG int i=1;i<=n;i++)lg[i]=lg[i>>1]+1;
    	for(RG int i=1;i<=n;i++)lg[i]--;
    	for(RG int i=1;i<=n;i++)S[0][i]=height[i];
    	for(RG int k=1;p[k]<=n;k++)
    		for(RG int i=1;i+p[k-1]<=n;i++)
    			S[k][i]=min(S[k-1][i],S[k-1][i+p[k-1]]);
    }
    il int lcp(int l,int r){
    	l=rk[l];r=rk[r];if(l>r)swap(l,r);l++;
    	return min(S[lg[r-l+1]][l],S[lg[r-l+1]][r-p[lg[r-l+1]-1]]);
    }
    

    2:(Height[rank[i]]ge Height[rank[i-1]]-1)
    假设将所有后缀排序后,第((i-1))个后缀的前一个后缀是第(k)个后缀,
    (rank[i-1]=rank[k]+1)(Height[rank[i-1]]=LCP(k,i-1))

    我们可以知道第((k+1))个后缀一定在第(i)个后缀的前面(此时(Height[rank[i-1]]>1)),
    那么(LCP(k+1,i)=LCP(k,i-1)-1=Height[rank[i-1]]-1)
    因此(Height[rank[i-1]]-1=min_{j=1}^{rank[i]-rank[k+1]}Height[rank[k+1]+j])
    所以有(Height[rank[i]]ge Height[rank[i-1]]-1)

    有了这个性质我们就可以再(O(n))的时间内求出一个字符串的(Height)数组
    这里是代码

    int height[N],rk[N];//rk表示rank数组
    il void getheight(){
    	for(RG int i=1;i<=n;i++){//求后缀i的height
    		height[rk[i]]=height[rk[i-1]]?height[rk[i-1]]-1:0;
    		while(a[sa[rk[i]-1]+height[rk[i]]]==a[i+height[rk[i]]])
    			height[rk[i]]++;		
    	}
    }
    
    

    求区间内本质不同子串数目

    考虑(height)数组是怎样体现重复子串的。
    因为每个子串一定是原串后缀的一个前缀,那么当出现一对长为(len)的相同子串的时候,
    这对相同子串各自的后缀就一定会有长为(len)的一段前缀相同。
    (height)数组正好表示的就是后缀排序中相邻前缀的(lcp)长度。

    更为清晰的说法是,对于一个重复出现的子串,这些子串对应的后缀后缀排序后一定是连续的一段
    (height[i])记录的就是经过(i)的连续段的数量
    那么可以知道答案就是(frac{n(n+1)}{2}-sum_{i}height[i]),实际就是将每一段拆开来求。

    求最长可重叠重复(K)次字串

    重复子串即两后缀的公共前缀,因此重复子串的最大长度即(Height)数组的最大值
    (已经将后缀按照字典序排好,不可能有字典序不相邻的后缀其(LCP)反而更大的情况)
    求重复(K)次字串的最大长度,即求(Height)数组长度为(K)的连续一段的最小值的最大值
    使用单调队列即可完成

    int f[N],g[N],l=1,r,ans;
    int main()
    {
    	n=read();k=read();
    	for(RG int i=1;i<=n;i++)a[i]=read();
    	getsa();getheight();
    	for(RG int i=1;i<=n;i++){
    		while(l<=r&&f[r]>=height[i])r--;
    		f[++r]=height[i];g[r]=i;
    		while(l<=r&&g[l]<=i-k+1)l++;
    		ans=max(ans,f[l]);
    	}
    	printf("%d
    ",ans);
    	return 0;
    }
    

    求最长不可重叠重复子串问题

    考虑二分答案;
    检查字符串中是否有长度为(K)的不可重叠重复子串时,找到(Heightle K)的连续的一段,查出这段中后缀位置的最大值和最小值之差,
    如果某一段的差值(le K)则可行,否则不可行

    il bool check(int k){
    	for(RG int i=1,mn=inf,mx=0;i<=n;i++){
    		if(height[i]>=k){
    			mn=min(mn,min(sa[i-1],sa[i]));
    			mx=max(mx,max(sa[i-1],sa[i]));
    			if(mx-mn>=k)return 1;
    		}
    		else{mn=inf;mx=0;}
    	}
    	return 0;
    }
    int main()
    {
    	n=read();
    	for(RG int i=1;i<=n;i++)a[i]=read();
    	getsa();getheight();
    	RG int l=1,r=n,mid,ans=0;
    	while(l<=r){
    		RG int mid=((l+r)>>1);
    		if(check(mid))ans=mid,l=mid+1;
    		else r=mid-1;
    	}
    	printf("%d
    ",ans);
    	return 0;
    }
    
    

    求最长公共字串

    两个串

    将这两个串拼到一起,中间使用一个不在两个串中出现过的字符,之后求出这个合并串的(Height)
    那么最长公共字串就是不在同一个分串中的后缀(Height)的最大值

    int main()
    {
    	scanf("%s",s+1);n1=strlen(s+1);
    	s[++n1]='z'+1;scanf("%s",s+n1+1);n2=strlen(s+1);
    	for(RG int i=1;i<=n2;i++)a[i]=s[i]-'a'+1;
    	getsa();getheight();
    	for(RG int i=2;i<=n2;i++)
    		if((sa[i]<n1)^(sa[i-1]<n1))
    			ans=max(ans,height[i]);
    	printf("%d
    ",ans);return 0;
    }
    

    多个串

    仍然拼到一起,需要使用尺取法枚举左端点

    
    

    求重复次数最多的连续字串

    考虑枚举串长(L),可以知道(k=LCP(i,i+L)/L+1)就是以(i)开头,长度为(L)的循环次数
    对于每次枚举,只考虑枚举(L)的倍数部分
    如果不是(L)的倍数部分次数超过了(k),那么这个部分的结尾一定在
    ([i+L*k,i+L*k-1+LCP(i,i+L)\%L])之间
    此时(LCP(i-(k-LCP(i,i+L)\%L),i+k-(k-LCP(i,i+L)\%L)))一定只比(k)(1)
    于是每次枚举的复杂度为(O(frac{n}{L}))
    总复杂度是(O(nlogn))

    int main()
    {
    	scanf("%s",s+1);n=strlen(s+1);
    	for(RG int i=1;i<=n;i++)a[i]=s[i]-'a'+1;
    	getsa();getheight();
    	init();
    	for(RG int k=1,len,els;k+k<=n;k++)
    		for(RG int i=k;i+k<=n;i+=k){
    			len=lcp(i,i+k);ans=max(ans,len/k+1);
    			if((els=lcp(i+len%k-k,i+len%k)/k+1)==len/k+2)
    				ans=max(ans,els);
    		}
    	printf("%d
    ",ans);
    	return 0;
    }
    
  • 相关阅读:
    bootstrap表头固定
    JS:二维数组排序和获取子级元素
    JavaScript 变量声明提升
    一道经典面试题-----setTimeout(function(){},0)
    排序
    基础知识:Promise(整理)
    几个兼容相关的重要函数
    JSON
    关于由ajax返回的数据在for循环中只能取到最后一个数的问题
    如果要遍历除了for循环,你还知道什么?——JavaScript的各种遍历方式
  • 原文地址:https://www.cnblogs.com/cjfdf/p/9338220.html
Copyright © 2020-2023  润新知