• 后缀数组SA


    算法讲解

    后缀数组((SA,Suffix Array)),是将字符串的所有后缀排序得到的数组

    一些定义

    s(i,j):字符串(S)的第(i)为到第(j)

    suf(i):以 (i) 开头的后缀,即(s(i,n))

    sa[i]:将所有后缀排完序后,排名为 (i) 的是原串的第几位(开头位置)

    rk[i](rank_i)表示(suf(i))的最终排名

    经典图片

    img

    他们满足如下的重要性质:

    [sa[rk[i]]=rk[sa[i]]=i ]

    请务必理解并牢记这两个定义,否则下面会非常绕

    思想:倍增

    我们依次处理所有 (suf(i)) 的前 (2^0,2^1,2^2,cdots)位的排名,如果两个后缀的前(2^i)位相同,比较这两个后缀第 (2^i+1sim 2^{i+1})的大小。即比较(s(x+2^i+1,x+2^{i+1}))(s(y+2^i+1,y+2^{i+1}))

    而这个结果我们已经在前一轮计算前(2^i)位的时候计算过了

    这样我们就可以倍增求解,看看这张图可能会好理解一点

    但是排序如果使用(sort),那复杂度会多个(log)

    我们发现这个数字由两位拼成,这两位都(< n),考虑使用(O(n))的基数排序

    其实思想很好懂,关键是理解下面的代码

    理解代码

    先放代码

    int sa[N],rk[N],sec[N],tot[N],sz,rk1[N];
    inline void init(){
    	for(int i=1;i<=n;++i)rk[i]=s[i],++tot[rk[i]];
    	for(int i=1;i<=S;++i)tot[i]+=tot[i-1];
    	for(int i=n;i;--i)sa[tot[rk[i]]--]=i;
    }
    inline void bucsort(){
    	memset(tot,0,(sz+1)<<2);
    	for(int i=1;i<=n;++i)++tot[rk[i]];
    	for(int i=1;i<=sz;++i)tot[i]+=tot[i-1];
    	for(int i=n;i;--i)sa[tot[rk[sec[i]]]--]=sec[i],sec[i]=0;
    }
    inline void getsa(){
    	init();
    	for(int k=1;k<=n;k<<=1){
    		int cnt=0;
    		for(int i=n-k+1;i<=n;++i)sec[++cnt]=i;
    		for(int i=1;i<=n;++i)if(sa[i]>k)sec[++cnt]=sa[i]-k;
    		bucsort();
    		memcpy(rk1+1,rk+1,n<<2);
    		sz=1;rk[sa[1]]=1;
    		for(int i=2;i<=n;++i)
    			rk[sa[i]]=(rk1[sa[i]]==rk1[sa[i-]]&&rk1[sa[i]+k]==rk1[sa[i-1]+k])?sz:++sz;
    		if(sz==n)return;
    	}
    }
    

    数组的含义

    sa,rk:意义没有太大变化,只是把全局排名改成前(2^i)位的排名。对于前(2^i)位相同的,sa可以为任意符合条件的不同整数,而rk都相同

    sec[i]:第二关键字排名为 (i) 的数在原串中的位置

    rk1rk拷贝出来的数组,拷贝一遍的意义下面会说

    tot[i]:第一关键字为(i)的出现次数/前缀和

    初始化

    inline void init(){
    	for(int i=1;i<=n;++i)rk[i]=s[i],++tot[rk[i]];
    	for(int i=1;i<=S;++i)tot[i]+=tot[i-1];
    	for(int i=n;i;--i)sa[tot[rk[i]]--]=i;
    }
    

    (2^0)的情况,(rk)就是这一位的值,对(tot)做一次前缀和

    sa[tot[rk[i]]--]=i的意义是:对于第(i)位,它的排名为第一关键字小于等于自己的个数,sa[tot[rk[i]]]=i。自己用完了之后,与(i)(rk)相同的数中,等于自己的数的个数会减少,所以tot[rk[i]]--]

    找出 sec

    首先对于第(n-2^i+1sim n)位,第二关键字都是(0),肯定是最小的,放在最前面

    对于(sa[i]>2^x)的位置,它会作为(sa[i]-2^x)位置的第二关键字。因为(sa[i])是排名为(i)的后缀开头,保证了第二关键字的递增

    基数排序

    与上面极其类似

    (rk)相同的情况下倒叙枚举(sec),这样保证第二关键字在第一关键字相同的情况下满足条件

    求出新的rk

    首先确定(rk[sa[1]]=1)

    因为(rk)会变,把它复制一遍

    如果自己与前一个第一、第二关键字排名都相同,那么这两个依然是相同的,(rk[sa[i]]=rk[sa[i-1]]),否则,(rk[sa[i]]=rk[sa[i-1]]+1)

    记录有几个不同的值,如果有(=n)个就已经排序完成了,不必要继续了

    后缀数组的重要性质

    又一些定义

    height[i]suf(sa[i])suf(sa[i-1])的最大公共前缀(LCP)。即排完序后相邻两个的LCP。特别的,(height[1]=0)

    h[i](h[i]=height[rank[i]]​)。即(suf(i)​)与排名在(i​)之前的最近的一个的LCP

    重要性质

    • (LCP(i,j)=minlimits_{k=rk[i]+1}^{rk[j]}height[k](rk[i]<rk[j]))

    也就是说,求出(height)之后,可以(O(1))ST表求出两个串的LCP。求LCS(最大公共后缀)的话就把原字符串reverse一遍即可

    • (h[i]ge h[i-1]-1)

    首先(h[i-1]le 1)的时候显然成立

    否则把第一个字符删去,剩下的一定匹配。结合下面这个图更好理解

    于是我们可以在求出(sa)(rk)之后(O(n))求出height

    int height[N];
    inline void getheight(){
    	int k=0;
    	for(int i=1;i<=n;++i){
    		if(rk[i]==1)continue;
    		if(k)--k;
    		int pos=sa[rk[i]-1];
    		while(pos+k<=n&&i+k<=n&&s[pos+k]==s[i+k])++k;
    		height[rk[i]]=k;
    	}
    }
    

    这篇文章就大概讲这些了,具体的使用可以参见后缀数据结构的使用

  • 相关阅读:
    Assembly介绍
    How to be a Programmer
    (转) 展望未来,总结过去10年的程序员生涯,给程序员小弟弟小妹妹们的一些总结性忠告
    ClientScript.RegisterStartupScript()
    sql server日期时间转字符串
    GridView 全选
    C# 获取xml里的值
    web 点击按钮,根据点击确认进行下一步操作
    字符串宽相同
    FormClosing
  • 原文地址:https://www.cnblogs.com/harryzhr/p/14682417.html
Copyright © 2020-2023  润新知