Trie图和AC自动机的区别
Trie图是AC自动机的确定化形式,即把每个结点不存在字符的next指针都补全了。这样做的好处是使得构造fail指针时不需要next指针为空而需要不断回溯。
比如构造next[cur][i]的fail指针,cur为父节点,next[cur][i]为cur的儿子结点,如果是AC自动机,如果父亲结点tmp(tmp是cur的一份拷贝)的next[fail[tmp]][i]不存在时,需要让tmp不断回溯(即tmp = fail[tmp]),直到next[fail[tmp]][i]不为空时,才让fail[next[cur][i]] = next[fail[tmp]][i]。
如果是Trie图,那么直接让fail[next[cur][i]] = next[fail[cur]][i]就可以了,因为Trie图已经补全了next指针。
但是不管是Trie图还是AC自动机,它们的fail指针的指向都是一模一样的。所以不管是用Trie图还是AC自动机都可以构造fail树。不过Trie图比AC自动机好写多了,所以我一直都是写Trie图而不是自动机。
fail指针的性质
要能够灵活使用Fail树,首先需要了解fail指针的性质,所以先说下fail指针都有哪些性质。
每个结点的fail指针都指向自己的最长后缀,那么很重要的一个性质就是让一个结点cur的fail指针不断回溯向上走,直到碰到根结点为止,那么回溯时经过的结点所代表的字符串都是结点cur所代表的字符串的后缀。
什么是Fail树
下面的第一幅图是AC自动机,第二幅图是Fail树。之所以第一幅图是AC自动机而不是Trie图的原因是Trie图太特么难画了。不过具体的原理还是没有变的。
可以看出Fail树其实就是将AC自动机的next指针去掉,然后反转fail指针的指向所构造出来了,而且可以肯定这一定是一棵树 ,所以称之为Fail树。
Fail树的一个性质是,某个结点所对应的字符串肯定是其儿子结点,孙子结点. . .所对应的字符串的后缀。
Fail树的应用
如果有n个字符串,所有字符串的长度加起来不超过$10^6$,有m个查询,要查询第x个字符串在第y个字符串中出现了多少次。
如果是使用AC自动机查询,可以直接对字符串构建AC自动机,然后让y去走AC自动机,对于走过的结点,把其权值加1。那么要查询x在y中出现了多少次,便要从底层开始,顺着fail指针把权值上传。然后只要查询x结点的权值是多少就知道x在y中出现了多少次。每次查询的复杂度是O(tot+len[y]),其中tot是AC自动机的结点总数。
如果是使用Fail树进行查询,那么只要查询所有子结点的权值和就好了,子结点的权值和可以使用dfs序和树状数组来维护。然后同样让有去走AC自动机,将走过的结点的权值加1,只不过现在是用树状数组来维护权值。那么要查询x在y中出现了多少次,只要进行一次区间查询就可以了,即只要查询x结点的所有子结点就好了(根据fail树的性质),因为其dfs序号是连续的,所以是一次区间查询。可以将查询按照y排序,然后对具有相同y的查询一起查询。每次查询时间复杂度是O(len[y]+log(tot))。
该文章在我的个人博客地址是:http://www.alphaway.org/post-440.html