您的位置:首页 > 其它

后缀自动机复习小记

2016-01-19 21:28 471 查看

前言

为什么叫复习小记?因为之前学的时候没有完全理解QAQ。

什么是SAM

SAM就是后缀自动机,它满足以下性质:

1、从根节点沿着边走到达任意一个结点走出的字符串都是原文本串的一个子串,而且两种不同的走法走出来的字符串一定不相同。

2、一个结点可以接受新后缀的意思是从根节点走到这个结点所得出的所有字符串,均为原文本串的后缀。

3、pre数组可以看做fail指针。并满足如果p可以接受新后缀,pre[p]也可以接受新后缀。

下面还将用到step数组,表示从根节点走到一个点的最长路长度。

那么对于文本串S,我们一个字符一个字符的添加进后缀自动机就能得到S的自动机。其思想为已知字符串t的后缀自动机,往t的SAM加入新的后缀x可以得到tx的SAM。

我们考虑如何加入一个新后缀x。

首先,新建一个点np来表示新后缀。找到可以接受后缀的最后一个结点last,那么令p=last,不断令p沿着pre跳。由于性质3,所有p的x儿子为空的结点均可往np连边。但如果跳到一个p,p已经有了x儿子为q,该怎么办?

情况1:step[q]=step[p]+1,证明经过q一定得经过p,且是由p直接到q。这个时候假设把q看做np,那么走到p后沿着x边走到q就相当于走到p后沿x走到np。那么由于把q当作了np,所以原本从p走到np组成的字符串等价于从p走到了q,那么既然前者是文本串的后缀,后者也是文本串的后缀。因此q可以接受新后缀。pre[np]=q。

情况2:step[q]>step[p]+1,这时候不能沿用情况1的做法,因为从p走到q是文本串的后缀不能代表走其他路到q也是文本串的后缀。此时的q不一定能够接受新后缀。(情况1走到q一定得经过p,而情况2不能保证)。我们可以仿照情况1,即造出一个nq当作q的替代点,复制q所有信息,然后令step[nq]=step[p]+1,那就变成情况1了,pre[np]=nq。根据性质3,因为可以从p到nq(到达nq必经p),而从p也能到达q,所以从根节点走到q的路线包含了从根节点走到nq的路线,因此若q可以接受新后缀nq一定可以。pre[q]=nq。

这就是构造了。复杂度是o(n)的。

Trie上建SAM

例题是诸神眷顾的幻想乡。

大概是现在有许多字典树,然后要求字典树走出来的字符串集合本质不同的字符串数量。这个容易想到,找到每一个叶节点,把对应根节点到这个叶节点走出的字符串都连接在一起(中间用神奇字符阻隔),然后求sa求height,就是经典的后缀数组问题了。

可是后缀数组又慢,这个连接出的字符串又可能很长,怎么办?

我们可以trie上建sam,方法是对于每一个x,其在字典树中的父亲就是last。那么建出sam后呢?下面会讲解,先送上本题代码。

type node=record
son:array['a'..'j'] of longint;
end;
trie=record
a:array['a'..'j'] of longint;
bz:boolean;
fa:longint;
end;
var
g:array[0..2000000] of node;
f:array[0..8000000] of trie;
pre,step:array[0..2000000] of longint;
s:array[0..2000000] of int64;
i,j,k,l,t,n,m,tot,top,last,p,q,np,nq,ans,pot:longint;
c,d,h,next,go:array[0..200000] of longint;
root,wdc,fj:longint;
ch:char;
sh:array[0..200000] of char;
procedure addedge(x,y:longint);
begin
inc(top);
go[top]:=y;
next[top]:=h[x];
h[x]:=top;
end;
procedure inse(x:longint);
var
ch:char;
t:longint;
begin
if x>top then exit;
t:=wdc;
wdc:=f[wdc].a[sh[x]];
if wdc=0 then
begin
inc(pot);
wdc:=pot;
f[pot].fa:=t;
f[t].a[sh[x]]:=pot;
for ch:='a' to 'j' do
f[pot].a[ch]:=0;
end;
inse(x+1);
end;
procedure add(x:char);
begin
begin
inc(tot);
np:=tot;
step[np]:=step[last]+1;
p:=last;
while (p>=0)and(g[p].son[x]=0) do
begin
g[p].son[x]:=np;
p:=pre[p];
end;
if p=-1 then pre[np]:=0
else
begin
q:=g[p].son[x];
if step[q]=step[p]+1 then pre[np]:=q
else
begin
inc(tot);
nq:=tot;
step[nq]:=step[p]+1;
for ch:='a' to 'j' do
g[nq].son[ch]:=g[q].son[ch];
//g[nq].fg:=g[q].fg;
pre[nq]:=pre[q];
pre[q]:=nq;
pre[np]:=nq;
while (p>=0)and(g[p].son[x]=q) do
begin
g[p].son[x]:=nq;
p:=pre[p];
end;
end;
end;
last:=np;
end;
end;
procedure dfs(x:longint);
var
ch:char;
begin
if s[x]<>0 then exit;
for ch:='a' to 'j' do
if g[x].son[ch]<>0 then
begin
dfs(g[x].son[ch]);
s[x]:=s[x]+s[g[x].son[ch]]+1;
end;
end;
procedure dg(x,y:longint);
var
t:longint;
begin
inc(top);
sh[top]:=chr(ord('a')+c[x]);
if (d[x]=1)and(y<>0) then
begin
wdc:=root;
inse(1);
dec(top);
exit;
end;
t:=h[x];
while t<>0 do
begin
if go[t]<>y then dg(go[t],x);
t:=next[t];
end;
dec(top);
end;
procedure build(x:longint);
var
ch:char;
begin
for ch:='a' to 'j' do
if f[wdc].a[ch]<>0 then
begin
last:=x;
wdc:=f[wdc].a[ch];
add(ch);
build(last);
wdc:=f[wdc].fa;
end;
end;
begin
assign(input,'data.in');reset(input);
root:=0;
f[root].fa:=0;
pot:=0;
readln(n,m);
for i:=1 to n do
read(c[i]);
for i:=1 to n-1 do
begin
readln(j,k);
inc(d[j]);
inc(d[k]);
addedge(j,k);
addedge(k,j);
end;
top:=0;
for i:=1 to n do
if d[i]=1 then dg(i,0);
wdc:=root;
pre[0]:=-1;
build(0);
dfs(0);
writeln(s[0]);
end.


求第k小子串

已经建好sam了,现在要求第k小子串(相同算一个)。

那这个好办,只要一个小dp就好了,根据性质1容易知道。那么诸神眷顾的幻想乡就很容易解决了。

如果要求相同子串算多个呢?

这里有一道例题弦论,题目是给出文本串然后求第k小子串(ca=0表示相同算一个ca=1表示相同算多个)

我们可以用size[x]表示x从根节点走到x的所有字符串的出现次数。

比如abaaba

a显然是出现了4次,ab出现了2次。

根据性质3,pre当作fail指针来理解,则按照pre建树后,以x结点为根的子树大小就是size[x]。

为什么?

例如abaaba。这里懒得上图,大家可以在草稿纸上把sam与pre树建出。

a的出现次数当然是1+aa出现的次数+ab出现的次数。

而fail指针跳回结点1的分别代表了aa与ab。

这里我可以说一个结论:后缀i与后缀j在sam中走一遍终止的结点分别是i’与j’,假如后缀i是后缀j的前缀,那么从j’沿着pre跳一定能到达i’。根据性质3可证。

于是有了size数组(初始时所有size[last]=1),就可以类似ca=0的情况求出sum数组,然后找出第k小。

ca=0和ca=1的情况可以合并起来打,ca=0的时候只要让所有结点size都为1即可。

求size时是逆着拓扑序枚举(栈小递归会爆),sam的拓扑序其实就是把step排序。

参考程序:

#include<cstdio>
#include<algorithm>
#define fo(i,a,b) for(i=a;i<=b;i++)
#define fd(i,a,b) for(i=a;i>=b;i--)
using namespace std;
typedef long long ll;
const int maxn=5*100000+10;
int step[maxn*2],g[maxn*2][27],pre[maxn*2],cnt[maxn*2],a[maxn*2];
ll size[maxn*2],sum[maxn*2];
int i,j,k,l,t,n,m,last,tot,ca;
char ch;
void insert(int x){
int p=last,np=++tot;
step[np]=step[last]+1;
while (p>=0&&g[p][x]==0){
g[p][x]=np;
p=pre[p];
}
if (p==-1) pre[np]=0;
else{
int q=g[p][x];
if (step[q]==step[p]+1) pre[np]=q;
else{
int nq=++tot;
int i;
fo(i,0,25) g[nq][i]=g[q][i];
pre[nq]=pre[q];
step[nq]=step[p]+1;
pre[np]=nq;
pre[q]=nq;
while (p>=0&&g[p][x]==q){
g[p][x]=nq;
p=pre[p];
}
}
}
last=np;
size[last]=1;
}
void getsum(){
int i,j;
fo(i,0,tot) cnt[step[i]]++;
fo(i,0,step[last]) cnt[i]+=cnt[i-1];
fo(i,0,tot) a[cnt[step[i]]--]=i;
fd(i,tot+1,1)
if (ca==1) size[pre[a[i]]]+=size[a[i]];else size[a[i]]=1;
size[0]=0;
fd(i,tot+1,1){
sum[a[i]]=size[a[i]];
fo(j,0,25)
if (g[a[i]][j]) sum[a[i]]+=sum[g[a[i]][j]];
}
}
void find(int x,int k){
int i;
while (k>size[x]){
k-=size[x];
fo(i,0,25){
if (!g[x][i]) continue;
if (k<=sum[g[x][i]]){
printf("%c",i+'a');
x=g[x][i];
break;
}
k-=sum[g[x][i]];
}
}
}
int main(){
freopen("string.in","r",stdin);freopen("string.out","w",stdout);
last=0;
pre[0]=-1;size[0]=1;
while ((ch=getchar())!='\n') insert(ch-'a');
scanf("%d%d",&ca,&k);
getsum();
if (sum[0]<k) printf("-1\n");else find(0,k);
}


后续

什么?你想用SA来做弦论?可以啊!只要逐位二分出范围就好了!不过我并没有打过,大家可以向大神牛samjia求救。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: