ac自动机学习笔记
nxt 是与当前串最长后缀在 trie 上出现的位置。
如何求 nxt,利用 BFS,\(\text{nxt}[\text{son}[i]['a']] = \text{son}[\text{nxt}[i]]['a']\)
如何匹配,有个天真的想法是枚举匹配串的前缀,看有多少串是这个前缀和后缀,但这样会算重,可恶!那么就打下标记吧!然后我们在 trie 上按要求的串搜下去,就这样吧,因为保证了最长前缀的性质,所以比较好证。
- 刚开始初始化第一层的 \(fail\) 指针为 \(root\) 。
- 如果不存在一个节点 \(i\),那么我们可以将那个节点设为 \(fafail\) 的值和 \(i\) 相同的儿子。保证存在性,就算是 0 也可以成功返回到根,因为 0 的所有儿子都是根。
- 无论 \(fafail\) 存不存在和 \(i\) 值相同的儿子 \(j\),我们都可以将 \(i\) 的 \(fail\) 指向 \(j\)。因为在处理 \(i\) 的时候 \(j\) 已经处理好了,如果出现这种情况,\(j\) 的值是第 2 种情况,也是有实际值的,所以没有问题。
- 实现时不记父亲,我们直接让父亲更新儿子。
\(nxt\) 数组是一个用来优化时间复杂度的东西,不是保证正确性的东西,所以我们的代码就是暴力加了剪枝,这样可能更好理解。
然后有个神奇的东西,我 ac 自动机是以 1 为根的,这样就会错掉。我猜测是一些莫名的地方以 0 为根的话一些没有儿子的会跳到 0 上从而继续跳下去。
ac 自动机上 dp,设 \(\text{dp}[i][j]\) 为 ac 自动机上第 \(i\) 位匹配到第 \(j\) 位。
例题:
(感谢@柳易辰的推荐)
每个串在模式串中出现的次数,按照模式串往下跳,然后加一下。然后 ac 自动机上跳就可以了。
离线的话就是傻子题。强制在线的话我们就不知道 trie 的状态,那就必须得每一次都更新 nxt。对于这种每一都要 \(O(n)\) 更新的,但是求答案只要 \(O(1)\)(也不是啦,平均下来就是了)我们可以使用根号平衡。这样是 \(O(26n\sqrt{n})\) 的,过不了。这题有个很逆天的二进制合并方法。就是我们每次创一个大小为 1 的 trie,然后不停合并。每次求和最多有 log 颗,然后每个数最多被求 log 次 nxt,合并也是启发式是 log 的,这貌似是个很厉害的技巧。删除的话也是 ac 自动机上减一下就行了。然后注意一下我们一个是 trie,一个是 ac 自动机。
P5319
ac 自动机上 dp 板子题,我们设 \(\text{dp}[i][j]\) 为做到了 ac 自动机上第 \(i\) 个点和字符串第 \(j\) 位的最大方案,然后枚举 \(i,j\) 然后向后更新,然后最后的在 \(x.size()\) 处更新,这样才能统计到答案。
P2414
想把这些字符串操作放到 trie 上,我们的 \(B\) 就是将指针上移,字符就是将指针右移,\(P\) 就是 ++。我们将询问排序,发现没用。考虑我们做了什么,我们在 ac 自动机上如果能跳到这里的话答案就 +1,这可以拿 70 分。从下面往上比较难,我们改变为 \(y\) 的子树中有多少 \(x\) 的节点。我们将询问离线下来,那么就是就是子树和,这个性质是因为串 \(x\) 在 ac 自动机上出现过决定的,如果没出现过的话那么 ac 自动机上的 \(son\) 会拓展。按照 \(x\) 排序,然后依次询问。即可做到 \(O(n\log n)\)。使用树剖!我们要做的就是一条链 +1,然后询问子树和。我们可以利用 dfs,这样就可以过了。注意,这个问题已经第二次了。就是 ac 自动机不等于 trie(可以看作任何情况下),所以要遍历 trie 的时候要保留原来的 trie。
没有强制在线的可持久化不是可持久化。把询问离线下来,然后发现询问之间是一种树的关系。这颗版本树上我们要加字符串,和减字符串,然后询问,也就是一条路上。这个怎么维护。一开始就把 ac 自动机建出来,然后加就是对应的 +1。询问的话是每一条链询问,这个用树剖即可。从根到点的询问可以将串的单点加变为子树和,查询变成单点查。
首先区间我们考虑它可不可以用前缀和搞掉,这显然是可以的。那我们就将询问拆成两个,然后将询问排序,然后暴力跳就可以了。哦,假了,哈哈哈。我们发现可能一个串被询问很多次,这样就会 T 掉。对于这样的问题,我们一般有两种解决的方案。比如说 \(x\) 在 \(y\) 中出现的次数。对 \(x\) 进行子树加然后暴力跳 \(y\) ,这样我们就是按照 \(x\) 排序,来处理询问的问题。还有一种是对于 \(y\) 进行单点加,然后对于要求的 \(x\) 进行子树求和(CF547E)。第一种适合 \(y\) 比较小,第二种适合 \(x\) 比较小。不妨使用根号平衡。对于那些长度大于 \(\sqrt{n}\) 的询问,询问不超过 \(\sqrt{n}\) 个,那么我们对于每个询问,先链加,然后再子树求和。对于长度小于 \(\sqrt{n}\) 的询问,我们就按照我们现在这样做即可。
书接上回!首先前缀和,然后对询问排序。要询问的就是 \(k\) 的子树中有多少 \(1\dots r\) 所遍历到的节点,这样直接暴力加,然后单点查即可。
把所有 \(s_i + s_j\) 都建到字典树上,发现字典树的形态应该是一个森林状物。然后不会了。我们可以把答案拆成两部分,一部分是不跨越中转点的,还有一部分是跨越的。对于跨越中转点的,我们需要统计出 \(t\) 的前缀是多少串的后缀,\(t\) 的后缀是多少串的前缀。第一个比较简单,第二个我们把 \(t\) 和所有串都反转了,然后就变成了相同的问题。这个可以在字典树上跑然后跳 \(nxt\),然后随便优化下就行了。\(s_i + s_j\) 在 \(t\) 中的出现次数。我们枚举断点,也就是有多少串是 \(t\) 断点前面串的后缀,有多少串是 \(t\) 断点后面串的前缀。
先离线,建出 ac 自动机。显然是暴力跑询问的串。我们在字符集中进行子树加,然后单点查即可。

浙公网安备 33010602011771号