后缀自动机感性理解
后缀自动机实是不是很好理解, 尤其是直接看大段的证明, 不知道它在干什么, 可能会有点懵
那我先介绍一下我的感性理解好了, 大家看这篇文章可能会更好的理解其他人的博客QAQ
前置芝士 : trie树
先来讲一下假后缀树($ n^2 $) , 由于它是假的所以很容易理解, 不用怕
偷图.jpg
对于一个字符串(例: \(bananas\))来说, 把它的所有后缀($bananas, ananas, nanas, anas, nas, as, s \() 一个一个的插入trie树, 并在末尾打上end标记, 就是暴力的后缀\)trie\(了, 显然时空复杂度均为\)O(n^2)$
那么可以说后缀自动机是对它很大程度上的优化了, 也就是对重复出现的子串和后缀进行压缩等操作, 使时空复杂度骤降为 \(O(n)\), 并具有后缀树的一些性质
关于子串的理解: 对于字符串S的一个子串, 你可以理解它为((S的一个后缀)的前缀), 没毛病
也就是如果S的一个子串在trie上一定是一条从根节点开始的路径
有啥用呢
- 找一个子串的出现次数: 在trie上找到它的路径, 在它下方的end标记之和, 也就是它是多少个后缀的前缀
- 找一个子串第一次(最后一次)出现位置, 同上, 就是向下走到深度最大(深度最小)的end标记
- 统计本质不同子串的个数, 即trie树上节点的个数
开始切入后缀自动机:
后缀自动机神奇的连了一大堆边, 成功的压缩了空间与时间
他有一个小性质: 从始节点开始, 走任意路径, 到终止节点的路径均是原字符串的一个后缀, 终止节点可能不止一个
先贴一张图, 来自zjp大佬的博客, 方便理解性质:
几个必备要素:
endpos(x): 它是一个集合, 表示一个子串的所有结束位置(可能有许多结束位置, 因为会有本质相同子串), 如果两个子串的\(endpos\)相同, 那么这两个子串属于一个"状态" , 同时他俩一个是另一个的后缀
len(x) : 对于一个状态所表示的一堆字符串, 他们最长的那个的长度, 同时这些字符串按长度排个序, 长度是连续的整数
后缀link:
设一个A状态如("abab", "bab", "ab") 那么"b"就是状态中没有的最长后缀, 即"abab"(或bab"等)的最长的且没有在该状态出现的后缀
那么我将A状态向"b"状态所在的B状态连有向边, 叫做link, 如果从一个状态不断的跳link, 那么就会遍历一个字符串的所有后缀
转移函数: 在一个状态的末尾加一个字符使它转向另一个状态, 可以证明在同一个状态的字符串在末尾加一个字符后还在同一个状态
下面来讲构造:
考虑从前往后一个一个加入字符, 即增量法, 这样就保证了每加一个字符都满足后缀自动机的性质
设当前最长串为S[1...i-1], 现在加一个字符S[i], 我们要干的事就是让它的所有后缀都能从起点开始走一条路径表示出来
S[1...i]肯定是一个新状态, 因为他是最长的, 设这个转态为np
因为前面S[1...i-1]已经构造好了, 我们从状态p = {s[1...i-1]}开始往前跳, 刚才说了, 往前跳的过程中会遍历它的所有后缀, 那么我们直接从以前的状态向他连一条边, 就可以从以前的状态转移到他了, 虽然这还是O(\(n^2\))的错误解法, 但给我们提供了不错的思路
设ch[s]['a'~'z']为它的转移函数, 如ch[s]['a']表示从s状态加一个'a'字符转移到哪个转态
设加入字符c, 向刚才一样跳link, 设到了状态A, 如果ch[A][c] == 0, 直接让ch[A][c] = np, 然后继续跳link, 最后如果跳到了根节点, 那他的link就是初始状态
for (; p && !ch[p][c]; p = f[p]) ch[p][c] = np; if (!p) f[np] = 1;
如果碰到了ch[A][c] = q(\(q \not= 0\)), 分两种情况
如果len(p) = len(q) + 1, 那么使q成为终止状态, np向q link一下
否则, 原先p中的字符串集就不再有相同的\(endpos\), 因为从A转移过来的串也是S[1...n]的后缀, 所以这部分\(endpos\)会多一个(i), 这个状态就会分裂, 因此我们新建一个状态\(nq\)去让多出的部分转移, \(p\)将转移到\(nq\), \(nq\)再转移到\(np\), 同时\(q\)和\(np\)都将向\(nq\) \(link\)
看看图理解一下(大佬讲的很好)
代码:
void add(int c) { int p = las, np = las = ++cnt; zhi[cnt] = 1; len[np] = len[p] + 1; for (; p && !ch[p][c]; p = f[p]) ch[p][c] = np; if (!p) f[np] = 1; else { int q = ch[p][c]; if (len[q] == len[p] + 1) f[np] = q; else { int nq = ++cnt; for (int i = 0;i < 26; i++) ch[nq][i] = ch[q][i]; len[nq] = len[p] + 1, f[nq] = f[q]; f[q] = f[np] = nq; for (; p && ch[p][c] == q;p = f[p]) ch[p][c] = nq; } } }