• 后缀数组及其应用


    这里简单介绍一下倍增算法((O(nlogn)))求后缀数组
     


     
     
     

    定义

    定义(S)是长度为(n)的字符串,后缀(suf[i])(S[isim n])

    (S)的所有后缀按字典序排序:

    • (rk[i](rank[i]))(suf[i])的排名;
    • (sa[i])指第(i)名的后缀是(suf[sa[i]])
    • (height[i])指第(i)名和第(i-1)名后缀的最长公共前缀((lcp))

     
     

    算法原理

    sa数组求法

    求得(sa)数组也就求得了(rk)

    首先我们可以很容易想到一个快排实现的算法,如果基于逐字符比较的算法时间是(O(n^2logn))

    我们也可以想到一个优化,用(Hash+)二分代替逐字符比较,这样时间是(O(nlog^2n))

    另外的想法:

    可以把每个后缀看成由(ASCII)码构成的数列,用基数排序来实现,这样时间是(O(n^2))的;

    想办法优化:

    因为待排序的字符串都是(S)上面的后缀,它们是有相同部分的;

    这样我们可以引入这样的一个算法:

    1.对于每个后缀的第一个字符进行一次排序,得到它们的排名作为每一个后缀的第一关键字;

    2.对于每个后缀的前两(1+1)个字符进行一次排序,(将(suf[i])的前两个字符(S[i],S[i+1]),看做一个整体进行排序,由于第一步已经的到了长度为1的子串的排名,这里就等价于将它们的排名进行合并后再排序),再得到它们的排名作为每一个后缀的第一关键字;

    3.对于每个后缀的前三个(1+2)个字符进行依次排序,再得到它们的排名作为每一个后缀的第一关键字;

    4.对于每个后缀的前五个(1+4)个字符进行依次排序,

    ······

    直到所有排名都互不相同;

     
    看图片更好理解,就是一个不停排序合并再排序的过程,每次合并时都能保证是上一步已经解决的问题,且对于每一个后缀都能不重不漏地包含所有字符,因此这样的排序效果上是等价于暴力基数排序的;

    又由于合并长度是倍增实现的,所以最多进行(log_2n)次排序,而每次排序都使用位数为(2)的基数排序,就能使算法时间复杂度维持在(O(nlogn))级别;

    这部分的的代码

    #include<bits/stdc++.h>
    #define ll long long 
    #define mp make_pair
    using namespace std;
    
    const int N=100005;
    char c[N];
    int n,m=127;//m是值域的初始大小  
    int tub[N];//桶 
    int fk[N],psk[N];//注意!!!fk(first key)是某个位置的一关键字 
    //而psk(pos of second key)是 排名为i的二关键字所在的位置 
    int sa[N],height[N],rk[N];
    //ll ans;
    
    inline int read()
    {
    	int x=0,fl=1;char st=getchar();
    	while(st<'0'||st>'9'){ if(st=='-')fl=-1; st=getchar();}
    	while(st>='0'&&st<='9') x=x*10+st-'0',st=getchar();
    	return x*fl;
    }
    
    inline void rsort()//基数排序 
    {
    	for(int i=1;i<=m;i++) tub[i]=0;//清空桶 
    	for(int i=1;i<=n;i++) ++tub[fk[i]];//将第一关键字加入桶中 
    	for(int i=1;i<=m;i++) tub[i]+=tub[i-1];//求桶的前缀和,得到一关键字的排名 
    	for(int i=n;i>=1;i--) sa[tub[fk[psk[i]]]--]=psk[i];//按照从后到前的顺序,依据二关键字的排名得到目前的sa 
    }
    
    inline void SA()
    {
    	for(int i=1;i<=n;i++) fk[i]=c[i],psk[i]=i; //初始 
    	rsort();//第一次排序 
    	for(int k=1;k<=n;k<<=1)//合并的长度 
    	{
    		int cnt=0;
    		for(int i=n-k+1;i<=n;i++) psk[++cnt]=i;//这一部分位置是没有能与其合并的 ,其二关键字应是最小的 
    		for(int i=1;i<=n;i++)
    			if(sa[i]>k) psk[++cnt]=sa[i]-k; //这里的sa[i]会被某个位置合并,这个位置是sa[i]-k
    		//这一部分的二关键字是按照顺序加入的 
    		rsort();
    		swap(fk,psk);//将fp放到psk转存,方便得到下一次的一关键字 
    		fk[sa[1]]=1;
    		cnt=1;
    		for(int i=2;i<=n;i++) //如果两个位置的一关键字和二关键字都相同,则排名不同,需要+1  
    			fk[sa[i]]=(psk[sa[i]]==psk[sa[i-1]]&&psk[sa[i]+k]==psk[sa[i-1]+k])?cnt:++cnt;
    		if(cnt==n) break;//如果已经所有排名不同了就可以退出 
    		m=cnt;//更新值域 
    	}
    }
    
    int main()
    {
    	n=read();
    	scanf("%s",c+1);
    	SA();
    //	get_height();
    //	ans=(ll) n*(n+1)/2;
    //	for(int i=1;i<=n;i++)
    //		ans-=height[rk[i]];
    //	printf("%lld",ans);
    	return 0;
    }
    

    height数组的求法

     
    主要依靠一个性质:

    (height[rk[i]] ≥ height[rk[i-1]]-1)

    感性证明:

    假设排在(suf[i-1])前面的那个串是(suf[k])它们的最长公共前缀长度为(height[rk[i-1]])

    (suf[i])(suf[i-1])少了它的第一个字符,可得(suf[i])(suf[k+1])的最长公共前缀是(height[rk[i-1]]-1)

    (suf[i])(suf[k+1])在排序后不一定在一起(即(height[rk[i]])不一定是它们的最长公共前缀),

    可是就算不在一起,在排好序的后缀中(suf[k+1])(suf[i])之间的后缀都一定满足,与(suf[i])的最长公共前缀至少是(height[rk[i-1]]-1)

    那么一定有(height[rk[i]] ≥ height[rk[i-1]]-1)

    这样就可以得到一个(O(n))的代码

    inline void get_height()
    {
        int k=0;
        for(int i=1;i<=n;++i) rk[sa[i]]=i;
        for(int i=1;i<=n;++i)  //i是位置不是排名 
        {
            if(k)--k;//-1
            int j=sa[rk[i]-1];//找到排名前一个的位置 
            while(j+k<=n&&i+k<=n&&c[i+k]==c[j+k])++k;//继续匹配 
            height[rk[i]]=k;
        }
    }
    

     
     
     


     
     

    一些应用

    询问任意两个后缀的lcp

    可以得知(suf[i])(suf[j])(rk[i]<rk[j]))的(lcp)长度就是(minlimits_{k=rk[i]+1}^{rk[j]}height[k])

    这就变成一个区间最小值的问题了;

     
     

    本质不同的子串

    (S)的所有子串个数是(n*(n+1)/2)

    (S)的所有子串也可以由,所有后缀的前缀得来;

    其中相同的有(sum height[i])个;

    所以有:

    [ans=dfrac{n imes (n+1)}{2}-sum height[i] ]

     
     

    两个串的最长公共子序列

    (Dp)会超时的那种;

    一个简单的做法是:

    将两个串合并成一个串,并且中间用一个不会出现的字符隔开(如%),求出新串的后缀数组;

    求出一个最大的(height[i]),满足(sa[i])(sa[i-1])在不同的串里面;

    这个做法可以推广到多个串的最长公共子序列,但时间也会增加;

     
     
     

    出现次数大于等于k的最长子串

    找出,长度为(k-1)的序列的(height[])的最小值,的最大值;

  • 相关阅读:
    MySQL之ERROR 1558 (HY000): Column count of mysql.user is wrong.解决方案
    手动发布本地jar包到Nexus私服
    Git的常用命令
    手游录屏直播技术详解 | 直播 SDK 性能优化实践
    【容器云】十分钟快速构建 Influxdb+cadvisor+grafana 监控
    【容器云】传统金融企业的 Docker 实践
    直播推流端弱网优化策略 | 直播 SDK 性能优化实践
    云存储之覆盖上传——七牛云
    「视频直播技术详解」系列之七:直播云 SDK 性能测试模型
    「视频直播技术详解」系列之六:现代播放器原理
  • 原文地址:https://www.cnblogs.com/yudes/p/SA.html
Copyright © 2020-2023  润新知