SAM-后缀自动机-个人总结

i207M

2018-11-16 14:43:07

Personal

~~JZ入门题:在SAM上跑SG函数~~ ~~感觉SAM写起来比AC自动机还简单~~ ~~然后考试的时候就忘了AC自动机~~ 写SAM的时候细节还是挺多的,一定要随时用眼睛看 **SAM初始的tot要设为1!!!** **看12月集训Day2.** ---------------------- 因为很重要,所以写在前边。 设k为字符集大小。 ### 普通的SAM(一个串的SAM) 如果每个点的trans数组开成字符集大小:时间$O(n)$,空间$O(nk)$ 此时: SAM中的状态总数(right集合相同为一个state)上界:$2n-1$,达到方法:$abbbb...bbb$ SAM中的转移总数上界:$3n-4$,达到方法:$abbb...bbbc$ [出处](https://cp-algorithms.com/string/suffix-automaton.html) 如果k很大,使用map存trans:时间$O(n\log n)$,空间$O(n)$(因为使用map,所以常数很大) 但是如果使用主席树实现$O(1)$继承trans,$O(\log n)$修改的话,时间复杂度应该还是$O(n\log n)$ ### GSAM-广义SAM(对Trie建) [PDF](https://static.googleusercontent.com/media/research.google.com/zh-CN//pubs/archive/35395.pdf)或者看结尾 **当字符集的大小是不可忽略的时候。不能在Trie上建SAM** 好我们看[另一篇](http://dwjshift.logdown.com/posts/304570): k为字符集。 用BFS离线建: 时空$O(nk)$ 用DFS在线建: 时间$O(nk+dfs)$,空间不变。 ------------- ## SAM的处理,往往和可持久化线段树合并维护right集合有关 ### 把所有子串按照字典序大小输出 建好SAM后直接按字典序dfs一遍。 ## 求字典序第k小子串 SPOJ SUBLEX 重复的子串不多次计数。 注意到后缀自动机中,虽然一个结点可能被当成多个结点的儿子,但这些连边都是满足拓扑序的,就是说,可以预处理出到达某个结点时,往下走可以得到多少个字符串,这样的预处理用拓扑排序+递推即可。这样的话,询问就是O(n)的复杂度。 在写这道题时,我在纠结如何预处理这个size,甚至想要去重,但是**因为一条路径就唯一对应一个子串,因此我们统计的其实是一个点之后的路径条数**,直接拓扑排序+DP即可。 ------------ 但如果重复的子串多次计数呢?[TJOI2015]弦论 很简单,之前的方法是从每个节点必有一个子串产生,而现在每个点有多个子串产生,是多少?是right集合的大小!于是同样的做法就行了。 size不要和上一个算法弄混!求right集合时只给主干点+1。 ### 一定要按拓扑序DP! 记得加上每个点的贡献。跑答案的时候,记得每走过一个边就要--k,因为有一个子串在这里结束了。 ## 求多个串的最长公共子串 首先选出第一个串,建立SAM,然后我们要把其他串放到SAM上跑,方式和AC自动机很像,**我们把parent树看作fail树**,如果这个点没有这个儿子,就跳fa,同时更新len。 然后如果跳到了有这个儿子的节点,就++l(l表示已经匹配的长度),x变成儿子; 否则从rt开始重新跑。 然后考虑多串:我们其实是把每个点的匹配长度取min之后就是答案。于是我们维护每一个串的匹配的最长长度,然后和len和之前的min取min。 ```cpp for(ri i=1; i<=n; ++i) { int t=s[i]-'a'; while(x&&!ch[x][t]) x=fa[x],l=len[x]; if(x) ++l, x=ch[x][t]; else x=rt,l=0; mx[x]=max(l,mx[x]); } ``` ### 字符串的最小表示法 但是明明有更好写的双指针啊,而且不在意字符集大小。 大字符集非要用SAM?map!嫌弃map的复制太慢?~~主席树~~弃疗! ### 求本质不同的子串数量 就是每个点的$\max -\min +1$,怎么求min?$\min=len[fa]+1$! **字符串题,多组数据有关数组一定要清空干净!最好字符串char数组也memset一下** ### 求多个数字字符串本质不同的子串的数字和 hdu4436 多个字符串?把每个字符串用特殊字符(比如10)接起来,跑一边SAM,建出DAWG,然后考虑DP: 我们不能走10边;**对于根节点不能走0边,因为忽略前导零 !**然后维护从根节点到点x的方案数cnt和数字和sum,转移的话:$sum[ch[x][i]]+=sum[x]\times 10+cnt[x]\times i$。 其实不用特殊字符接起来也行,直接建广义SAM跑就可以了,很快。 ### [AHOI2013]差异 $\displaystyle \sum_{1\leqslant i<j\leqslant n}\text{len}(T_i)+\text{len}(T_j)-2\times\text{lcp}(T_i,T_j) $ 一个很简单的方法是求出Height数组之后,这个问题就转化为了$[2,n]$的所有子区间的最小值的和,可以通过单调栈求出每个点作为最小值的区间,然后统计贡献。记得单调栈一边是>一边是>= 如果非要用SAM怎么办?Parent树的LCA实际上是两个前缀的最长公共后缀,所以我们需要对反串建立SAM,然后两个点的LCA就是最长公共前缀,DP计数一下就可以了。 ### [APIO2014]回文串 ~~开O2防MLE???~~ Manacher和SAM的优秀结合! 首先建SAM,然后跑Manacher,得到若干本质不同的回文子串,然后我们要查询它们的出现次数,怎么查?子串$S[l...r]$的出现次数就是SAM上r对应的节点的祖先中,$len>=r-l+1$且最浅的祖先,这就是它的出现次数。 ### [USACO17DEC]Standing Out from the Herd 好题,专门写一个。 ### [ZJOI2015]诸神眷顾的幻想乡 求树上所有路径的不同子串个数,叶子数量<=20,N<=1e5 本来想$20\times 20$的枚举两个叶子然后把树上路径塞进去,看题解之后恍然大悟:直接从每个叶子出发遍历一棵树,然后把路径插入广义SAM就可以了! ## SP8093 给定n个模板串,以及m个查询串,依次查询每一个查询串是多少个模板串的子串。 我们给SAM上每个点一个颜色,那么问题就转化成了一个点的parent树的子树的颜色个数,可以用dsu on tree或者树状数组+扫描线解决。 ## 串 现在给定你n个字符串,询问每个字符串有多少子串(不包括空串)是所有n个字符串中至少k个字符串的子串(注意包括本身)。 像上一题一样进行预处理,然后把每个串放在上面跑,如果一个点的次数小于k就不停的跳fa,然后答案累加上$len[x]$即可。 由于一步只能向下走1,所以复杂度是有保证的。这道题我为了加快,写了一个倍增,挂掉了,很奇怪。 ### CF666E Forensic Examination SAM+数据结构 ### CF700E Cool Slogans 思维好题 ## BZOJ3796 Mushroom追妹纸 见另一篇博客。 ## pty的字符串 先把S建出SAM,求出每个点的rigth集合的大小, 然后把Trie树放到SAM上跑,每经过一次一个点就把这个点的cnt++,然后求cnt在parent树上的子树和,答案即为$\sum size_{right}\times sum_{cnt}\times (len[i]-len[fa[i]])$ 但是这个是不对的!因为匹配到一个点时,匹配的长度不一定是这个点的len,所以这个点的贡献要特殊计算,贡献的最后一部分为匹配的$ml-len[fa[i]]$。这样cnt只需要给fa加cnt。为什么fa及祖先就不用特殊考虑?因为它们的len一定小于ml。 实际写了一下,有很多需要注意的地方: 1. 这不是Trie树!会有重边! 2. 因为S太长了(8e6)所以必须对Trie建立SAM,吧S放在上面跑。 3. 因为是广义后缀自动机,所以要拓扑排序。 ## [SCOI2012]喵星球上的点名 见另一篇博客。 ## CF1063F 一眼看上去是一个神题,但是用到的算法并不难。 我们把串反过来,序列也反过来。 首先,我们每次选出来的串,一定一个是另一个的后缀**或前缀**。换句话说,我们的序列从前到后长度依次递增。 设$dp[i]$表示以i结尾的序列的最长长度。考虑如何求出$dp[i]$。首先这个满足二分的性质,考虑check。因为我们已经知道,它的前一个一定是前缀或者后缀,于是我们枚举两种情况。剩下的就是求一个串出现的所有位置的dp值最大值了,对Parent树的dfs序建线段树即可。 我们发现,我们能够用来更新dp值的位置右端点也是单调不减的,于是用一个指针维护即可。 --------------------- 先丢定义: ![捕获.PNG](https://i.loli.net/2018/12/28/5c25e5d6cbca2.png) ![捕获.PNG](https://i.loli.net/2018/12/28/5c25e5f880524.png) 结论: ![捕获.PNG](https://i.loli.net/2018/12/28/5c25e6244013b.png) ![捕获.PNG](https://i.loli.net/2018/12/28/5c25e63a41c3f.png) ![捕获.PNG](https://i.loli.net/2018/12/28/5c25e65300403.png) ![捕获.PNG](https://i.loli.net/2018/12/28/5c25e66eb6d6a.png) 说人话: 定义k阶GSAM为:最小的k使得取出原来所有串的后k个字符,它们两两不同。n为字符串个数。 则**最简**k-GSAM的时空复杂度均为$O(\sum len+kn)$。 但是Trie树上的串,虽然两两的后缀都不同,貌似k=1,但是!按Trie建出来的不是最简的GSAM!因为会有很多重合的情况!况且Trie树也不是最简状态自动机。