又开坑?
论文看不懂,网上资料又过于简单?……
反正还是不懂。找到几个资料,云说好好看论文最好,但是真的看不懂(ZZS:我不知道你们看不看得懂clj的课件,因为我看不懂),子陵小孩子也是直接背,kpm大神直接看网上就懂其精髓(orz!!)。还是再去看论文吧……&
2015.02.18 0:40 在云的帮助下看完了……只能称丽洁姐太神了,明天看能不能整理下……
后缀自动机
学了后缀数组之后,写出了个比较快的版本,然后又听到kpm大神虐完后缀数组又去虐sam了。于是终于在某天决定开坑。(随便坑了kpm大神一个中午和云一天)。
网上资料好多,严谨的看不懂(orz clj),通俗的看了心虚(orz 19世纪30年代的空间的百度空间)。
先上一些网址吧(看多了只会让自己更乱?!)
http://hi.baidu.com/myidea/item/142c5cd45901a51820e25039?qq-pf-to=pcqq.group
http://blog.sina.com.cn/s/blog_70811e1a01014dkz.html
http://blog.sina.com.cn/s/blog_7812e98601012cim.html
云这样评价clj的论文:至今能找到的最最科学的东西了
所以我还是按照论文说说一个蒟蒻的理解吧
什么是后缀自动机(sam)
也就是一个能识别字符串s的后缀的自动机。当且仅当sam(x)=true是字符串x是s的后缀(同时sam也可以识别s的所有的字串哦!)
Sam就是建立一个最简状态后缀自动机。
定义了很多东西:
令ST(str)表示trans(init,str)。就是初始状态开始读入字符串str之后,能到达的状态。
令Reg(A)表示自动机A能识别的字符。由于我们现在建的是字符串s的后缀自动机,所以Reg(A)就是s的所有后缀。(或者这样表示Reg(ST(s))就是s的所有后缀)
分析:
首先如果某个字符串x是s的字串,那么在这个字符串x后加上一些字符后就有可能成为s的后缀,而且有可能有不同种方法使它成为s的后缀,也就是说它可能是s的多个后缀的共同前缀!
比如说我们现在建立起s的sam了。然后读入了字符串s1,那么现在sam就走到ST(s1),那么x如果可以被ST(s1)识别,那么x必须是s的后缀,现在走到了ST(s1)可以识别x,那么s1+x也是s的后缀。那么可以说一个状态ST(s1)可以识别的一定是s的后缀。
现在我们来举个看个例子。
S=ABBBABBABBBBBABBA(直接用论文)
其中BBA出现的位置有
BBABBABBBBBABBA
BBABBBBBABBA
BBABBA
BBA
也就是BBA一共出现在{ [3,6) , [6,9) , [12,15) , [15,18) }
我们记右端点为right,也就是right(BBA)={6 , 9 , 15 , 18}
然后我们发现有些时候有些字符串的right集合一样的,那我们干脆就记在一起,即有个状态f表示所有right相同的字符串的集合,这个集合就记为right(f)吧。
然后字符串的出现的区间中无论长度如何,从ri往前数不超过li所得到的字符串一定是一样的,比如上面的BBA={ [3,6) , [6,9) , [12,15) , [15,18) },那么6,9,15,18往前数两个字符都是BA。也就是给个right集合,然后再个适合的长度,就可以确定一个字符串了。
所有right集合相同的字符串构成一个状态f,那么对于一个状态,也比如存在一个长度范围,【l,r】,这个范围内从ri外前数多上个字符所构成的字符串的right集合还是right(f)。我们记为【min(f),max(f)】。
现在有两个状态a和b,他们的right集合就分别叫Ra和Rb吧。
如果Ra和Rb有交集,即其中有相同的ri,那么他们的适合长度区间一定不会有交集(否则某个字符串的right即等于Ra又等于Rb但是Ra<>Rb,显然不可能)。但是Ra和Rb又有交集啊?所以,只能是这样——一个是另一个的真子集。比如上面的BBA和BBBA(两个属于不同的状态),可以发现BBBA的right是BBA的right的子集,同时BBA是BBBA的后缀。So,“个串的right集合,要么不相交,要么一个是另一个的真子集。”
结果可以发现right集合其实构成了一个树(大集合的儿子们是小集合)。而且还可以发现儿子的min一定是爸爸的max+1(如果不是那爸爸的max+1才是儿子min,矛盾嘛)。
P.s 子串的性质。
首先每个子串都有属于某个状态,求这个子串出现的个数就变成了求这个子串所在状态的right集合的大小。但是维护这个集合的大小有点麻烦,我们想到right集合是个树形结构,right集合就是它的儿子们right的和,而儿子们的right又是孙子们right的和……最后就是right集合就是它的子树中所有叶子节点的right的和,那么直接用dfs序统计不就好了?!
终于到了构造
怎么做一只sam?
我们先来看状态间的转移:一个状态a,它的Right(a)={r1,r2,r3,r4,…..,rn},然后它可以沿一条标号为c的边转移到状态b,那么right(b)必须是right(a)中那些s[ri]=c的ri+1组成的集合。即right(b)={ri+1 | s[ri]=c }。
然后如果a有一条标号为c的边,那么他在树中的父亲f也一定有一条标号为c的边。比如a表示AA,f表示A,那么a有一条编号为C的到它的儿子,那么f中可能有一条C到f的儿子(AC是AAC的子串嘛)。
现在来想怎么做。
令当前字符串为T,新字符为x,令T的长度为L。
现在用sam(T)构造sam(Tx)。新增了一些字符,这些字串都是Tx的后缀,而Tx的后缀,就是T的后缀后面加一个x。
记v1,v2,v3,。。。,vk,v为right集合中包括L的节点,然后这些v必然是从曾孙子到曾祖父,也就是从最小的v1,(right(v1)={L})开始到root的整条路径。
设加入x后新的节点为np,即ST(Tx)=np,然后right(np)={L+1}(这些都是显然的)。
举个例子:
如果当前要加入x,然后第一个x的边为vp(也就是AAAAA所在状态),然后q就是AAAAAX所在状态,right(q)={ri+1 | s[ri]=x }={8,23},长度是【1,7】。如果让q直接插入L+1,那么right(q)={8,23,38},但是长度变成了【1,6】。所以不能直接插入。
AAAAAAXAAAAAAAAAAAAAAXAAAAAAAABAAAAAX
AAAAAAXAAAAAAAAAAAAAAXAAAAAAAABAAAAAX
显然如果max(q)=max(vp)+1就没有任何问题啦。
解释?:(雾)直接插会让长度变短,但是无论多短,都要>=max(vp)+1,如果原来就是max(q)=max(vp)+1,就不用担心长度变短的影响了……
但是max(q)>max(vp)+1怎么破?
接下来考虑节点nq,在转移的过程中,结束位置L+1是不起作用的,所以trans(nq)就跟原来的trans(q)是一样的,拷贝即可。
(后面觉得丽洁姐讲的挺好的,其实是我太弱了似乎又理解得不那么透彻?)
procedure add(x:longint);
var
i,new,now,old,more:longint;
begin
new:=addpoint;
now:=last;
step[new]:=step[now]+1;
while (now>=0) and (son[now,x]=-1) do begin
son[now,x]:=new;
now:=pre[now];
end;
last:=new;
if now<0 then pre[new]:=0
else
if step[son[now,x]]=step[now]+1 then
pre[new]:=son[now,x]
else begin
old:=son[now,x];
more:=addpoint;
for i:=0 to 25 do son[more,i]:=son[old,i];
step[more]:=step[now]+1;
pre[more]:=pre[old];
pre[old]:=more;
pre[new]:=more;
while (now>=0) and (son[now,x]=old) do begin
son[now,x]:=more;
now:=pre[now];
end;
end;
end;
(就不要吐槽代码丑了)