学习笔记::后缀自动机
终于看懂了,写一点东西以防忘记,不保证讲的都对
1.什么是自动机?自动机是指对于一个自动机A,如果能识别字符串S,那么A(S)=true,否则A(S)=false,就像AC自动机能够识别给定文本中特定的文本一样。
1.5:后缀自动机和AC自动机一样,有两个比较重要的东西,AC自动机一般我们会碰到在自动机上DP和利用fail树的性质,后缀自动机也是一样,我们可以在自动机本身上搞一些东西,也可以在Parent树上搞一些东西。
2.后缀自动机:和字面意思一样,就是能识别一个字符串的所有后缀,其实还能识别字符串的所有子串。
我们想一个最裸的能够识别所有后缀的东西,比如说我们建立一个trie,把每个后缀都插到里面去,这样的确能识别所有的后缀和子串,但是trie的节点数有n^2个,太大了。
于是我们就想办法节省状态
对于后缀自动机的每个节点表示的状态,我们保存5种信息:
1.ch,也就是trans,表示当前状态能够通过添加字符达到的其他状态,通俗的讲就是当前有一个字符串集合,我们给某些字符串末尾加一些字符,这样这些字符串就变成了另一个状态。
2.Right:其实这个东西并没有直接保存在每个节点上,下面会说为什么。Right集合保存的是一些位置,这些位置向前拓展一定长度形成的字符串是相同的,且每个节点的Right集合要么包含要么交集为空,这个可以到PPT里去看证明。
3.Max:这个就是上面Right集合每个位置能拓展的最大长度。
4.Min:为什么会有最小长度呢?不应该最小长度是0吗?Right集合要保证一个性质,就是说在一个长度范围[l,r]内,每个位置向前拓展[l,r]长度,所有字符串都相等,且所有满足这个性质的位置都在这个Right集合内,不多也不少。如果拓展的长度不断缩小的话,会有越来越多的位置生成的字符串相同, 那么这些长度区间对应的位置集合是不同的,自然不能视为一个Right集合。其实这个也不用直接记录。
5.Parent树:上面提到了Right集合只能互相包含或交集为空,那么如果一个集合a包含另一个集合b,我们就可以把a看做b的父亲,这样就形成了一棵有向树,根节点为拓展长度为0的位置集合,自然包括了所有的位置。注意这里的父亲和儿子的关系和ch没有什么关系,没有说a是b的par,那么b是a的ch。每个Right集合对应一个长度区间[Min,Max],在Parent树上,设一个节点为u,父亲为fa,那么有Max(fa)=Min(x)-1,因为每次集合元素的增加都是由于长度变小而导致更多的位置被加入,而长度的增加是连续的,所以区间也是连续的。也就是说Parent树上从根开始的路径上每个节点的长度范围是连续的,所以对于每个点我们只用保留Max就行了。
上面提到了关于Right集合保存的方法,如果直接每个点保存的话内存是n^2,自然是不行的。我们观察一下Parent树,其实Parent树中每个叶子结点都只对应了一个位置,那么对于每个节点他子树中的叶子就构成了这个节点的Right集合。
3.构建:构建这个东西是在线的,也就是说我们可以随时插入一个字符。
每次我们新加入一个字符c,那么对于所有S的后缀都被加上了一个c,所以我们不光需要新建一个节点,还要更新其他有关节点的信息,例如ch转移状态。新插入的节点产生一个新的Right集合,位置是当前最后位置,长度范围是[len,len],就是对应新的字符串,那么val[x]=val[last]+1,然后我们还要更新之前对之前结尾产生的新字符串。因为last是上一次的末尾对应的Right集合,那么我们沿着last向Parent树上爬,遇见的集合都包括last,那么怎么更新的?如果一个节点没有ch[c],那么我们可以直接让ch[c]=x,但是碰到已经有ch[c]?就是说之前已经有当前集合字符串添加字符c生成的状态了,设这个节点为p,我们看能不能把x添加进p->ch[c]的Right集合,什么情况下能呢?ch表示添加字符而进行状态转移,那么现在添加一个字符,相当于把p的所有字符串加上c变成ch[c]中的元素,我们当然不能让Max(ch[c])改变,因为有一个结论Max(ch[c])>Max(p),所以说加上一个字符后Max不能改变,那么Max(p)+1==Max(ch[c])才行,那么其他的情况怎么办呢?我们新建一个节点nq,nq表示ch[c]和end的并集,因为end+c的Max=Max(p)+1,又因为Max(ch[c])>Max(p),所以Max(nq)=Max(p)+1,然后就是把ch[c]的所有信息拷贝到nq上,并且将ch[c]和end的par设为nq,nq的par设为q的par,最后就是把原先ch[c]=q变成=nq,就结束了。
SAM
namespace SAM { int root, sz, last; struct node { int par, val; map<int, int> ch; } t[N]; int nw(int x) { t[++sz].val = x; return sz; } void iniSAM() { sz = 0; root = last = nw(0); } void extend(int c) { int p = last, np = nw(t[p].val + 1); while(p && !t[p].ch[c]) t[p].ch[c] = np, p = t[p].par; if(!p) t[np].par = root; else { int q = t[p].ch[c]; if(t[q].val == t[p].val + 1) t[np].par = q; else { int nq = nw(t[p].val + 1); t[nq].ch = t[q].ch; t[nq].par = t[q].par; t[q].par = t[np].par = nq; while(p && t[p].ch[c] == q) t[p].ch[c] = nq, p = t[p].par; } } ans += t[np].val - t[t[np].par].val; last = np; } } using namespace SAM;
妮玛我板子写错了怪不得有个地方看不懂