后缀自动机复习小记
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求救。相关文章推荐
- 284. Peeking Iterator
- Vi编辑器和Vim编辑器的区别及联系
- POJ 3186 Treats for the Cows
- 编写自文档化代码
- Java学习第6天:面向对象(2)对象实例
- 编写自文档化代码
- hdu 4057 AC自己主动机+状态压缩dp
- mysql的性能优化总结
- Java学习第6天:面向对象(1)简介对象
- spark 1.6 MLlib
- 帆软连接默认数据库的参数
- HTML
- 2016 系统设计第一期 (档案一)MVC form数据提交
- git、github for window、tortoisegit的区别
- asp.net中配置文件的详解
- android TV - Creating a Catalog Browser,Providing a Card View
- service总结
- Material Design整合使用之TabLayout+ViewPager
- 用户登录验证
- [iOS问题归总]iPhone上传项目遇到的问题