后缀数组学习笔记

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

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

首先做一些基础介绍:

后缀数组(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;
}


 

posted @ 2018-09-21 16:07  lleozhang  Views(141)  Comments(0Edit  收藏  举报
levels of contents