• 后缀数组学习笔记


    后缀数组是一个很迷的字符串算法...

    后缀数组的特点是:思想嘛...还行 代码嘛...很乱

    首先做一些基础介绍:

    后缀数组(sa)是一个数组(废话),他的作用是存储字典序排名为i的后缀的位置(即后缀的起点)

    而后缀数组常常与rank数组同步计算,其中rank数组是起点为i的后缀的排名

    既然如此,我们只要求出sa和rank其中之一,我们就可以求出另一个

    接下来,我们就来考虑一下怎么求

    首先,如果我们完全暴力应用sort排序,那么时间是必炸无疑的

    所以我们要考虑另一种方法

    倍增!

    考虑如下字符串:abababab

    我们对最初的字符串进行一个排序,排序结果如下:

    a b a b a b a b
    1 2 1 2 1 2 1 2

    为什么要倍增呢?因为我们希望利用上一次的排序结果进行下一次的排序,将时间优化为O(nlog2n)

    所以我们倍增一下,得到:

    ab ba ab ba ab ba ab b
    1,2 2,1 1,2 2,1 1,2 2,1 1,2 2,0

    (这里其实只进行了一个操作,就是将每个字符和他后面那个字符合并,然后记录排序关键字)

    可是有个问题,这里有两个关键字,怎么排?
    这就涉及到后缀数组中很重要的一个内容:基数排序!

    举个例子:排序22,23,34,35,36,17.15这几个数

    我们显然能看到一种方法:首先按十位比较,十位大的值一定更大,所以我们把他们分成(22,23)(34,35,36)(17,15)三组

    然后排好序,就是(17,15)(22,23)(34,35,36)

    接下来对每组内部进行比较,这次按照个位排序,排好序之后就是(15)(17)(22)(23)(34)(35)(36)

    这就是一个基数排序的思想,即如果我们想比较两个关键字,且这两个关键字有优先级,我们就可以用基数排序的思想来解决

    于是我们重排一遍,结果就是:

    ab ba ab ba ab ba ab b
    1 3 1 3 1 3 1 2

    所以接下来,我们就可以不断倍增来做了。接下来是这样:

    abab baba abab baba abab bab ab b
    1,1 3,3 1,1 3,3 1,1 3,2 1,0 2,0

    再排一遍,就是:

    abab baba abab baba abab bab ab b
    2 5 2 5 2 4 1 3

    继续:

    abababab bababab ababab babab abab bab ab b
    2,2 5,4 2,1 5,3 2,0 4,0 1,0 3,0

    于是:

    abababab bababab ababab babab abab bab ab b
    4 8 3 7 2 6 1 5

    我们发现所有值都不同,也就不必再比下去了,于是结束后缀排序,得到rank数组(注意这个是rank数组!)

    如果想得到sa数组,也很简单:sa[rank[i]]=i

    但是,其实我们在后缀排序的模板中,是先求的sa,然后构造的rank...

    但思想其实是一样的。

    于是代码如下(接下来会有对代码的讲解,否则你会发现代码超出了你的想象!):

    #include <cstdio>
    #include <cmath>
    #include <cstring>
    #include <cstdlib>
    #include <iostream>
    #include <algorithm>
    #include <queue>
    #include <stack>
    using namespace std;
    int sa[1000005];
    char s[1000005];
    int rank[1000005];
    int has[1000005];
    int f1[1000005],f2[1000005],f3[1000005];
    int l,m=127;
    void get_sa()
    {
    	for(int i=1;i<=l;i++)
    	{
    		f1[i]=s[i];
    		has[f1[i]]++;
    	}
    	for(int i=2;i<=m;i++)
    	{
    		has[i]+=has[i-1];
    	}
    	for(int i=l;i>=1;i--)
    	{
    		sa[has[f1[i]]--]=i;
    	}
    	for(int k=1;k<=l;k<<=1)
    	{
    		int tot=0;
    		for(int i=l-k+1;i<=l;i++)
    		{
    			f2[++tot]=i;
    		}
    		for(int i=1;i<=l;i++)
    		{
    			if(sa[i]>k)
    			{
    				f2[++tot]=sa[i]-k;
    			}
    		}
    		for(int i=1;i<=m;i++)
    		{
    			has[i]=0;
    		}
    		for(int i=1;i<=l;i++)
    		{
    			has[f1[i]]++;
    		}
    		for(int i=2;i<=m;i++)
    		{
    			has[i]+=has[i-1];
    		}
    		for(int i=l;i>=1;i--)
    		{
    			sa[has[f1[f2[i]]]--]=f2[i];
    			f2[i]=0;
    		}
    		memcpy(f3,f1,sizeof(f3));
    		memcpy(f1,f2,sizeof(f1));
    		memcpy(f2,f3,sizeof(f2));
    		f1[sa[1]]=1;
    		tot=1;
    		for(int i=2;i<=l;i++)
    		{
    			if(f2[sa[i]]==f2[sa[i-1]]&&f2[sa[i]+k]==f2[sa[i-1]+k])
    			{
    				f1[sa[i]]=tot;
    			}else
    			{
    				f1[sa[i]]=++tot;
    			}
    		}
    		if(tot==l)
    		{
    			break;
    		}
    		m=tot;
    	}
    	for(int i=1;i<=l;i++)
    	{
    		printf("%d ",sa[i]);
    	}
    }
    int main()
    {
    	scanf("%s",s+1);
    	l=strlen(s+1);
    	get_sa();
    	return 0;
    }

    上面的sa和rank数组的含义已经介绍过了,而has是一个桶,f1,f2是第一、第二关键字

    稍微做一些解释:

    (这是不压行版本的,接下来会有一个压行版的更好看)

    首先我在写的时候直接利用了ascll码(1-127),所以字符集大小订到了127

    然后我做一个初始化,把所有字符扔进对应的桶里(一个类似桶排的思想),同时初始化第一关键字为对应字符的acsll码

    然后我对桶做一个累加,就能知道某一个字符的初始排名了

    接下来我倒序枚举字符串,处理出初始的sa值

    然后进行倍增,设倍增的长度为k

    首先,对于位置在k之后的部分,他们的第二关键字都是0,所以要先扔进第二关键字的序列里

    接下来,如果有某个排名的sa比k大,这说明这个排名有可能作为某一个位置的第二关键字出现,所以也扔进去

    接下来把桶清空

    然后统计第一关键字后累加

    统计完毕之后,我们倒序枚举整个字符串,更新sa值

    如何更新?

    按第一关键字和第二关键字重排即可

    接下来就是统计了,比较好懂

    放上好看的压行代码,来自神犇guapisolo

    #include <cstdio>
    #include <cstring>
    #include <algorithm>
    #define il inline
    #define N 1005000
    #define inf 0x3f3f3f3f
    using namespace std;
    
    int len;
    char str[N];
    int tr[N],rk[N],sa[N],hs[N];
    il int check(int k,int x,int y)
    {
        if(x+k>len||y+k>len) return 0;
        else return (rk[x]==rk[y]&&rk[x+k]==rk[y+k])?1:0;
    }
    void getsa()
    {
        int i,cnt=0;
        for(i=1;i<=len;i++) hs[str[i]]++;
        for(i=1;i<=127;i++) if(hs[i]) tr[i]=++cnt;
        for(i=1;i<=127;i++) hs[i]+=hs[i-1];
        for(i=1;i<=len;i++) rk[i]=tr[str[i]],sa[hs[str[i]]--]=i;
        for(int k=1;cnt<len;k<<=1)
        {
            for(i=1;i<=cnt;i++) hs[i]=0;
            for(i=1;i<=len;i++) hs[rk[i]]++;
            for(i=1;i<=cnt;i++) hs[i]+=hs[i-1];
            for(i=len;i>=1;i--) if(sa[i]>k) tr[sa[i]-k]=hs[rk[sa[i]-k]]--;
            for(i=1;i<=k;i++) tr[len-i+1]=hs[rk[len-i+1]]--;
            for(i=1;i<=len;i++) sa[tr[i]]=i;
            for(i=1,cnt=0;i<=len;i++) tr[sa[i]]=check(k,sa[i],sa[i-1])?cnt:++cnt;
            for(i=1;i<=len;i++) rk[i]=tr[i];
        }
    }
    
    /*void get_h()
    {
        for(int i=1;i<=len;i++)
        {
            if(rk[i]==1) continue;
            for(int j=max(1,h[rk[i-1]]-1);;j++){
                if(str[i+j-1]==str[sa[rk[i]-1]+j-1]) h[rk[i]]=j;
                else break;
            }
        }
    }*/
    
    int main()
    {
        //freopen("a.in","r",stdin);
        scanf("%s",str+1);
        len=strlen(str+1);
        getsa();
        for(int i=1;i<=len;i++) printf("%d ",sa[i]);
        return 0;
    }
    
    
    
  • 相关阅读:
    android自定义通知栏遇到的问题
    写博客号的初衷
    模型转换遇关键字
    界面传值的四种方式
    button循环添加事件
    解析数据的步骤
    数组排序 (数组是有序容器,因此集合中只有数组才能排序。)
    集合遍历
    自定义view和自定义cell
    cell自适应高度
  • 原文地址:https://www.cnblogs.com/zhangleo/p/10764207.html
Copyright © 2020-2023  润新知