• 后缀数组原理浅析(转载自tqx)


    原博客链接

    一、概述

    后缀数组((SA,Suffix Array)),是将字符串的所有后缀排序得到的数组,主要包括两个数组:

    (sa[i])将所有后缀按字典序排序后第(i)小的后缀的开头位置

    (rk[i])表示从第(i)个字符开始的后缀(我们将它称为后缀(i))的字典序排名

    它们满足(sa[rk[i]]=rk[sa[i]]=i)

    一个例子(搬迁自后缀数组——处理字符串的有力工具 罗穗骞):

    img

    如果我们能快速求出(sa)(rk),那么我们能利用它们完成很多字符串题目,我们接下来介绍后缀数组的求法,然后再介绍一些常见应用:

    二、后缀数组的求法

    倍增求法

    最常见的一种求法,复杂度为(mathcal O(nlog(n))),这个算法的核心就是利用倍增的思想。

    • 思想

    首先我们先按照每个后缀的第一个字符对后缀进行排序,这相当于将这个字符串的每个字符进行排序

    显然,这样做会出现排名相同的后缀,接下来我们就要对这些字符串的第二位进行排序了。我们再排序一次吗?事实上,我们已经比较过了:因为后缀(i)的第(2)个字符正是后缀(i+1)的第(1)个字符,也就是说,后缀的第二个字符的排序就是它的下一个后缀的第一个字符的排序,利用这个排序作为第二关键字,我们就能得到前两位的排序了

    以此类推,现在我们知道了前两位的排序,自然也就知道了第(3-4)位的排序,于是用同样的方法就能求出前(4)位的排序,如此倍增下去,直到每一个后缀的排名都不相同,我们就完成了排序。

    对双关键字进行排序这件事,我们可以通过基数排序做到(mathcal O(n)),因此算法的复杂度是(mathcal O(nlog(n)))

    那么具体如何实现呢?我个人认为后缀数组的代码对初学者十分不友好,因此我们一点一点地来讲。

    • 代码理解

    在这个过程中,我们用(s[i])表示原字符串第(i)位,(rk[i])表示按第一关键字排序得到的结果,一开始我们排序的是第一个字母,那么(rk[i])就是(s[i])

    for(int i=1;i<=n;++i)
    	rk[i]=s[i],++c[rk[i]];//刚开始第一关键字就是该后缀的第一个字母 
    for(int i=2;i<=S;++i)
    	c[i]+=c[i-1];
    for(int i=n;i>=1;--i) sa[c[rk[i]]--]=i;
    

    这里(S)是目前排名集合的大小,这里我们用桶排序的思想,用(c[x])表示排名是(x)的字符串个数,做个前缀和之后(c[x])表示排名(le x)的字符串个数,然后我们就可以求出(sa)了,在出现相同排名时,我们现在不关心它们的内部排名,就直接让位置靠后的字符串排名较小,最后一个直接排名为(c[rk[i]]),然后将它(--)作为下一个排名相同的字符串的排名以保证排名互不相同

    接着开始倍增,枚举(k)表示目前我们已经知道前(k)位的排序,想要推出前(2k)位的排序

    int num=0;
    for(int i=n-k+1;i<=n;++i) y[++num]=i;
    for(int i=1;i<=n;++i)
    	if(sa[i]>k) y[++num]=sa[i]-k;//y[i]:第二关键字排名为i位的后缀的起始位置 
    

    这一段代码是求出第二关键字,即第(k+1-2k)位的字典序排序,我们用(y[i])表示第二关键字排名第(i)位的后缀的其起始位置,对于后缀(n-k+1-n),它们没有(k+1-2k)位的东西,因此它们直接排在最前面,紧接着枚举第一关键字的排名,被先枚举到的(sa[i])意味着后缀(sa[i]-k)的第二关键字排名靠前,因此我们按序加入(y)中。

    for(int i=1;i<=S;++i) c[i]=0;
    for(int i=1;i<=n;++i) c[rk[i]]++;
    for(int i=2;i<=S;++i) c[i]+=c[i-1];
    for(int i=n;i>=1;--i)
    	sa[c[rk[y[i]]]--]=y[i],y[i]=rk[i];
    //桶排序优先保证了第一关键字的排名,因为是从后往前考虑y所以说相对靠后的是第二关键字排行靠后的
    

    接下来开始基数排序,我们还是先按(rk)放在桶中,但这次对于(rk)相同的后缀我们不能随意排序了,要第二关键字靠后的排在后面,于是我们从后往前枚举(y[i]),这样先枚举到的第二关键字一定更大,于是给它较大的排名。(y[i]=rk[i])则是我们接下来要重新计算(rk),但会用到之前的(rk),于是我们直接用(y)保存下来。

    rk[sa[1]]=num=1;
    for(int i=2;i<=n;++i){
    	if(y[sa[i]]==y[sa[i-1]]&&y[sa[i]+k]==y[sa[i-1]+k])						             rk[sa[i]]=num;
    	else rk[sa[i]]=++num;
    }
    if(num==n) return ;
    S=num;
    

    接下来我们利用(sa)重新计算(rk),这时如果两个后缀的两个关键字都相同,那么直接使用(num),否则(num++),当(num=n)时所有后缀的排名互不相同,于是我们就完成了排序。

    • 洛谷模板题代码:

      #include<bits/stdc++.h>
      using namespace std;
      const int N=1e6+10;
      char s[N];
      int rk[N],y[N],sa[N],n,c[N],S=122;
      //千万牢记:sa[i]是当前排第i位的后缀的起始位置 
      //rk[i]是从i开始的后缀的排名 
      inline void getsa(){
      	for(int i=1;i<=n;++i)
      		rk[i]=s[i],++c[rk[i]];//刚开始第一关键字就是该后缀的第一个字母 
      	for(int i=2;i<=S;++i)
      		c[i]+=c[i-1];
      	for(int i=n;i>=1;--i) sa[c[rk[i]]--]=i;
      	for(int k=1;k<=n;k<<=1){//开始倍增 
      		int num=0;
      		for(int i=n-k+1;i<=n;++i) y[++num]=i;
      		for(int i=1;i<=n;++i)
      			if(sa[i]>k) y[++num]=sa[i]-k;//y[i]:第二关键字排名为i位的后缀的起实位置 
      		for(int i=1;i<=S;++i) c[i]=0;
      		for(int i=1;i<=n;++i) c[rk[i]]++;
      		for(int i=2;i<=S;++i) c[i]+=c[i-1];
      		for(int i=n;i>=1;--i)
      			sa[c[rk[y[i]]]--]=y[i],y[i]=0;
      		//桶排序优先保证了第一关键字的排名,因为是从后往前考虑y所以说相对靠后的是第二关键字排行考后的
      		swap(rk,y);rk[sa[1]]=num=1;
      		for(int i=2;i<=n;++i){
      			if(y[sa[i]]==y[sa[i-1]]&&y[sa[i]+k]==y[sa[i-1]+k])
      				rk[sa[i]]=num;
      			else rk[sa[i]]=++num;
      		}
      		if(num==n) return ;
      		S=num;
      	}
      }
      int main(){
      	scanf("%s",s+1);
      	n=strlen(s+1);
      	getsa();
      	for(int i=1;i<=n;++i) printf("%d ",sa[i]);
      	return 0;
      } 
      

    DC3算法与SA-IS算法

    二者都是(mathcal O(n))的算法,博主不太会,不过一般来说倍增算法就够用了,学习这两种算法,可以参考[后缀数组——处理字符串的有力工具 罗穗骞](后缀数组——处理字符串的有力工具 罗穗骞)学习(DC3),参考诱导排序与SA-IS算法学习(SA-IS)

    三、后缀数组的应用

    • 最长公共前缀(LCP)

      这是后缀数组最重要的应用之一,我们定义(LCP(i,j))表示后缀(sa[i])与后缀(sa[j])的最长公共前缀。

      为了求解它,我们给出一些性质

      • (LCP(i,j)=LCP(j,i))
      • (LCP(i,i)=n-sa[i]+1)

      这两条性质是显然的,于是我们可以只用考虑(i<j)的情况了。

      • (LCP Lemma)(LCP(i,j)=min(LCP(i,k),LCP(k,j))(1le ile kle jle n))

        证明:令(t=min(LCP(i,k),LCP(k,j))),那么(LCP(i,k)le t,LCP(k,j)le t)

        于是后缀(sa[i])与后缀(sa[k])的前(t)个字符完全相同,后缀(sa[k])与后缀(sa[j])的前(t)个字符相同,故后缀(sa[i])与后缀(sa[j])的前(t)个字符相同,故(LCP(i,j)ge t)

        同时因为如果(LCP(i,j)=q>t),那么(i,j)的前(q)个字符相等,因为(t=min(LCP(i,k),LCP(k,j))),所以要么(sa[i][t+1])(表示后缀(sa[i])的第(t+1)位)(<)(sa[k][t+1]),要么(sa[k][t+1])(<sa[j][t+1]),并且(sa[i][t+1]le sa[k][t+1]le sa[j][t+1]),所以(sa[i][t+1] ot=sa[j][t+1]),与假设矛盾,所以(LCP(i,j)=t)

      • (LCP Theorem)(LCP(i,j)=min(LCP(k-1,k)) ,kin(i,j])

        证明:有(LCP Lemma)(LCP(i,j)=min(LCP(i,i+1),LCP(i+1,j)),然后继续拆下去即可证明。

      于是,我们令(height[i]=LCP(i,i-1),height[1]=0),那么只要求出(height)我们就能求出(LCP)了,如何求出(height)呢?

      再令(h[i]=height[rk[i]]),于是(height[i]=h[sa[i]]),对(h[i]),我们有一个重要定理:

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

      • 证明:首先我们假设(sa[rk[i]-1]=j,sa[rk[i-1]-1]=k),于是(h[i]=LCP(j,i),h[i-1]=LCP(k,i-1)),于是我们只需证明(LCP(j,i)ge LCP(k,i-1)-1)

      • 如果后缀(k)与后缀(i-1)首字母不同,那么(LCP(k,i-1)-1=-1),那么无论(h[i])是多少定理都一定成立

      • 如果后缀(k)与后缀(i-1)首字母相同,那么分别去掉首字母后得到后缀(k+1)与后缀(i),必有(rk[k+1])(<rk[i]),于是(LCP(k+1,i)=h[i-1]-1),对于字符串(i),所有排名比它靠前的字符串中,与它相似度最高也就是(LCP)最大的一定是紧挨着它的字符串,即(j),但我们已知(k+1)排在(i)前面并且(LCP(k+1,i)=h[i-1]-1),那么必然有(LCP(j,i)ge LCP(k+1,i)=h[i-1]-1),即(h[i]ge h[i-1]+1)

      根据这一条定理,我们就可以直接枚举(rk[i])然后从(height[rk[i-1]]-1)作为起始点求(height[rk[i]])达到(mathcal O(n))求出所有(height)了:

      int height[N],h[N];
      inline void getheight(){
      	int k=0;
      	for(int i=1;i<=n;++i){
      		if(rk[i]==1) continue;
      		if(k) --k;
      		int j=sa[rk[i]-1];
      		while(j+k<=n&&i+k<=n&&s[j+k]==s[i+k]) ++k;
      		height[rk[i]]=k;  
      	}
      	for(int i=1;i<=n;++i) printf("%d ",height[i]);
      }
      
    NO PAIN NO GAIN
  • 相关阅读:
    洛谷 P6599 「EZEC-2」异或 题解
    线段树懒标记以及标记永久化的两种实现方法(模板)
    洛谷P3834 【模板】可持久化线段树 1 题解
    Codeforces 1012B Chemical table (思维+二分图)
    HDU 6386 Age of Moyu (最短路+set)
    CodeForces 739B Alyona and a tree (二分+树上差分)
    Codeforces 444C DZY Loves Colors (线段树)
    HDU 5441 Travel (离线dsu)
    Codeforces 1000E We Need More Bosses (边双连通+最长链)
    CodeForces 219D Choosing Capital for Treeland (树形DP)经典
  • 原文地址:https://www.cnblogs.com/zmyzmy/p/14676123.html
Copyright © 2020-2023  润新知