20241225北京总结
感觉课还是讲的挺明白的
QOJ8079. Range Periodicity Query
题意 : 有一个字符串 , \(q\) 次操作 , 每次在开头或末尾加入一个字符\(x\) ,询问其最长 \(border\) 长度。
首先讲一个重要结论 , 长为 \(len\) 的周期等价为长为 \(n-len\) 的 \(border\)
完了做一下以 \(len\) 为最短周期的 \(s\) , 可以通过二分哈希找到一段区间
接下来就是给你一堆区间 , 要求包含单点区间中最小的 , 直接扫描线
QOJ8701. Border
考虑修改完之后 , 对于 \(border\) 造成贡献
显然的 , 如果 \(a_i=b_i\) , 那么这次修改对于 \(border\) 没影响
否则直接枚举答案 , 前后缀不同的位置必须都被修改 (即只有一处不同) , 前后缀原本相同的位置一个都不能修改
然后就是一个对着 \(1\) 与 \(n-len+1\) 做最长公共前缀 , \(len\) 和 \(n\) 做最长公共后缀 , 找差的那个点后哈希验证前后是否一样
至于 \(LCP\) , \(hash\) + 二分可能会 \(T\) , \(exKMP\) 稳过 , 但我不会
border理论
上一些理性理解的东西 , 这些我之前都有一个模糊的认知 , 所以总结一下
长为 \(len\) 的周期等价为长为 \(n-len\) 的 \(border\) , 上文提到不再赘述
用 \(border(s)\) 来表示一个字符串 \(s\) 的所有 \(border\) 组成的集合 , 用 \(mxbd(s)\) 来表示 \(s\) 的最大 \(border\)
容易理解 , \(border\) 的 \(border\) 还是 \(border\) , 不断跳就完事了
失配树 / \(border\) 树
考虑这么一个问题 : 有 \(m\) 组询问 , 每组询问给定 \(p,q\) , 求 \(s\) 的 \(\boldsymbol{p}\) 前缀和 \(\boldsymbol{q}\) 前缀最长公共 \(\operatorname{border}\) 的长度
暴力的想法怎么做 , 直接用上述方法跳出两个串上所有 \(border\) , 之后再判断哪个最大 , 这与 \(LCA\) 跳所有祖先后找最近的思想不谋而合
所以我们直接按照 \(border\) 指针构建出一棵树出来 ( 其实它也是一个自动机 , 只有一个字符串的 \(AC\) 自动机的 \(fail\) 树 )
在 \(fail\) 树上跑 \(LCA\) 就行
之前做字符串半懂不懂的题也大概能理解了 , 例如 P3435 [POI2006] OKR-Periods of Words , 最大周期即为最小 \(border\) , 就是在 \(border\) 树上跳根就完事了
上面就是 \(border\) 树的一些东西 , 下面接着讲理论 , 不讲证明 , 感性认识一下就行
若 \(p\) , \(q\) 为串 \(s\) 的两个周期 , 且 \(p+q\leq |s|\) , 那么 \(\gcd( p , q )\) 也是 \(s\) 的周期
\(S\) 的长度不小于 \(|S|/2\) 的 \(border\) 长度构成一个等差序列
\(S\) 所有的 \(border\) 长度构成 \(O( \log(|S|) )\) 个等差数列
[NOI2014] 动物园
我们从三种视角来看这道题
普通 \(kmp\) 的角度来看 , 倍增算到限制
\(border\) 树的角度去看 , 本题就是要规定树的深度 , 在倍增向上跳的时候别跳过限制就行 , 注意到 \(1 , 2\) 做法其实是等价的
从 \(border\) 理论去看 , \(S\) 的长度不小于 \(|S|/2\) 的 \(border\) 长度构成一个等差序列
所以 , 若 \(nxt_i >i/2\) , 则答案减去 \(((i - 1)/2)/(i - nxt_i)\) 即可
ACAM AC自动机
经过昨天对于自动机的理解 , 我们也就明白了 \(AC\) 自动机就是 \(kmp\) 和 \(Trie\) 树再自动机意义上的结合
具体要怎么做 , 很简单 , 对于 \(Trie\) 树上每个链的后缀 , 我们去连一个最长真后缀和某个字符串前缀等同的 \(fail\) , 我们就发现它变成了一个类自动机结构的有向图 , 匹配失败就暴力跳
这样会遇到一些问题 , 例如在处理 \(aaaaaa....aaaa\) 这样的串的时候 , 就会不断暴力回跳 \(fail\) , 也就导致 \(ACAM\) 退化为一个做 \(n\) 次 \(kmp\)
怎么优化 , 我们先来提出一些性质 , 这个 \(ACAM\) 的 \(fail\) 指针构成一棵树 , 因为这些 \(fail\) 只会指向在 \(Trie\) 上比自己深度低的点 , 而且 \(fail\) 显然不会成环
我们发现每次都暴力跳 \(fail\) 很慢 , 为什么不把询问要问到的点全离线后一起求和 , 也就是为找到的结点打上标记
所以按照 \(fail\) 树 , 做一次拓扑排序或者 \(dfs\) , 把打上标记的结点求解
SAM 后缀自动机
\(SAM\) 能干些什么 , 在一个 \(DAG\) 上表示一个字符串的所有字串 , 一个很 \(trival\) 的方法是一个 \(Trie\) 树 , 我们就已经可以用 \(trie\) 树干一些很好的事情了 , 例如看一个字符串是否为另一个的子串 , 或是在上面做一些 \(dp\)
但他一共有 \(nm\) 个点 , 这就唐中唐了 , 时间和空间都无法支持 , 实际上 \(trie\) 树上的很多结点都可以合并 , 如ABA , ABABA , ABABABA , 他们都有共同后缀 \(ABA\) , 但作为 \(Trie\) 树来看 , 他们全分开建出来了
按我的理解来看 , 它是一棵树导致了它信息的不压缩性 , 而 \(SAM\) 很大一程度上接替了 \(Trie\) 树 , 而且他还是一个节点数最少的 \(DAG\)
一些 \(pre\) 知识
接下来抛几个概念 , 作为 \(SAM\) 的前置知识 , 首当其冲的 , 结束位置 \(\operatorname{endpos}\)
考虑字符串 \(s\) 中的字串 \(w\) , \(\operatorname{endpos}\) 就是所有的 \(w\) 在 \(s\) 中的结束位置 , 举个例子 , 若 \(s=ABABA\) , \(\operatorname{endpos}(BA)=\{3,5\}\) , 特别的 , 我们规定空串的 \(\operatorname{endpos}\) 为 \(\{-1,0,1,...,|s|-1\}\)
字符串 \(s\) 的两个非空子串 \(u\) 和 \(w\) ( 假设 \(|u|\leq |w|\) ) 的 \(\operatorname{endpos}\) 相同 , 当且仅当字符串 \(u\) 在 \(s\) 中的每次出现 , 都是以 \(w\) 真后缀的形式存在的
两个非空子串 \(u\) 与 \(w\) 的 \(\operatorname{endpos}\), 要么是包含关系 ( 即为它们有一个是另一个的后缀 ) , 要么就一点不相同 ( 交集为空 )
给出一个等价类的概念 , 即为所有 \(\operatorname{endpos}\) 相同的子串 , 考虑到一个等价类内的所有子串 , 按照大小顺序由小到大的排序 , 那么前一个串是后一个串的后缀 , 而且这些串的长度是在 \(Z\) 上连续的 , 覆盖了一整个区间 \([x,y]\)
这个看起来有点神秘 , 这里给出 \(oiwiki\) 的证明 : 由引理 \(1\) , 两个不同的 \(\operatorname{endpos}\) 等价的字符串中 , 较短者总是较长者的真后缀 , 因此 , 等价类中没有等长的字符串 . 记 \(w\) 为等价类中最长的字符串 , \(u\) 为等价类中最短的字符串 , 由引理 \(1\) , 字符串 \(u\) 是字符串 \(w\) 的真后缀
现在考虑长度在区间 \([|u|,|w|]\) 中的 \(w\) 的任意后缀 , 这个后缀也在同一等价类中 , 因为这个后缀只能在字符串 \(s\) 中以 \(w\) 的一个后缀的形式存在 ( 也因为较短的后缀 \(u\) 在 \(s\) 中只以 \(w\) 的后缀的形式存在 ) , 因此 , 由引理 \(1\),这个后缀和字符串 \(w\) 的 \(\operatorname{endpos}\) 相同
我们把 \(SAM\) 上的状态定义为一个等价类 \(v\) , 用一个等价类上最长的串作为代表 , 用 \(longest(v)\) 记录 , 等价类上最短的串记为 \(shortest(v)\) , 前者长度记为 \(len(v)\) , 后者长度记为 \(minlen(v)\)
后缀 \(\operatorname{link}\) , 就是 \(longest(v)\) 所在的状态向 \(minlen(v)-1\) 长度后缀所连的一条边 , 即 \(minlen(v)=len(link(v))+1\)
注意到不断向前跳 \(Link\) , 对他们的 \([minlen(v),len(v)]\) 取交为空 , 取并为 \([0,len(v_0)]\) , 也就是说 , 最后全跳到了空字符串 , 我们发现这符合一个树结构的特征 , 假设空字符串所在状态为 \(t_0\) , 那么 :
所有后缀链接构成一棵根节点为 \(t_0\) 的树 , 这个树上每个子结点的 \(subset\) 包含在它爹的 \(subset\) 当中
在了解这些概念和性质后 , 我们终于可以开始 \(SAM\) 的学习了
开始构建 \(SAM\)
一开始 \(SAM\) 只有 \(t_0\) , 令 \(last\) 为更新前字符串对应状态 , 设 \(cur\) 为目前状态 \(len(cur) = len(last)+1\)
如果 \(last\) 能到 \(c\) , 就记一下这个状态 , 如果找不到 , 建立一个 \(last\) 到 \(cur\) 的 \(link\)
如果 \(\operatorname{len}(p)+1=\operatorname{len}(q)\) , 我们只要将 \(\operatorname{link}(\textit{cur})\) 赋值为 \(q\) 并退出
否则 , 复制状态 \(q\):我们创建一个新的状态 \(\textit{clone}\) , 复制 \(q\) 的除了 \(\operatorname{len}\) 的值以外的所有信息 , 我们将 \(\operatorname{len}(\textit{clone})\) 赋值为 \(\operatorname{len}(p)+1\) , 把 \(\textit{cur}\) 指向 \(\textit{clone}\),也从 \(q\) 指向 \(\textit{clone}\)。
最终我们需要使用后缀链接从状态 \(p\) 往回走,只要存在一条通过 \(p\) 到状态 \(q\) 的转移,就将该转移给状态 \(\textit{clone}\)
以上结束后 , 更新 \(\textit{last}\) 为 \(cur\)
找终止状态的过程也是简单的 , 就是从整串的状态往回跑 , 路上所有都是终止
它是线性个状态的原因 , 考虑最差的情况 \(abbbbbbbb...\) , 也就只有 \(2n-1\) 个状态 , 线性个转移也从 \(abbbbbbbb...c\) 考虑 , 只有 \(3n-4\) 条边

浙公网安备 33010602011771号