后缀数组学习笔记
后缀数组是一个很迷的字符串算法...
后缀数组的特点是:思想嘛...还行 代码嘛...很乱
首先做一些基础介绍:
后缀数组(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;
}