后缀树, 后缀自动机
定义 \(endpos(s)\) 表示对于某个字串 \(s\) 在原串中的出现位置集合。
\(endpos\) 性质。
我们有两个原串的字串 \(s, t\), \(|s| \le |t|\), \(s\) != \(t\)
-
若 \(endpos(t) \in endpos(s)\), 则 \(s\) 为 \(t\) 的后缀。
证明
``` 存在一个位置 $x$, 满足 $x \in endpos(s)$ 且 $x \in endpos(t)$, 则在位置 $x$ 前面我们可以找到这两个串。 ``` -
若 \(s\) 为 \(t\) 的后缀, 则 \(endpos(t) \in endpos(s)\)
证明
``` 若存在一个 $x$, 满足 $x \in endpos(t)$ 且 $x \notin endpos(s)$, 则位置 [x - s.size() + 1, x] 为 $s$, 所以 $x \in endpos(s)$, 矛盾 ``` -
\(endpos(s)\) 与 \(endpos(t)\) 要么包含, 要么不交。
证明
``` 则存在一个位置 $x$, 满足 $x \in endpos(s)$, $x \in endpos(t)$, 类似性质 1 的证明, 可以得到 $s$ 为 $t$ 的后缀, 再通过性质2, 得 $endpos(t) \in endpos(s)$, 矛盾 ```
我们可以建出一颗 \(endpos\) 树(通过性质3), 每个节点父亲 \(endpos\) 包含这个节点。
一些关于这个树的性质
- 对于树上的每个节点, 表示的串长度连续且从大到小为后缀关系。可以通过反证法证明。
- 对于树上每个节点的最小值, 去除开头则为这个节点父亲的最长串。
我们记 \(len_u\) 表示一个节点 \(i\) 对应的后缀中最长串的长度, \(ch_{u, c}\) 表示考虑在 \(u\) 最长串的后面加一个字符 \(c\) 后转移到这颗树的最小值。
若不存在对应的点, 则 \(ch_{u, c}\), 则 \(ch_{u, c} = 0\)
证明一定转移到树上的一个点
其实发现就是 $endpos$ 集合集体加 1, 所以 $endpos$ 相同我们可以对于每个点记录一个所有串的最后一个字符位置, 就可以确定这些串。
构造
我们从前往后依次考虑加入每个字符。
我们设当前构成的串为 \(s\), 加入的字符为 \(c\), \(s\) 对应的点编号为 \(root\)。
由于前面的 \(endpos\) 集合不可能包含元素 \({|s| + 1}\), 所以我们知道字符串 \((s + c)\) 一定属于一个新的点, 我们不妨设该点的标号为 \(cyx\)。
考虑 \(cyx\) 的父亲是谁, 其实就是找到一个最长的字符串 \(T\), 使得 \(T\) 为 \((s + c)\) 的后缀且 \(T\) 长度最长。则 \(cyx\) 的父亲是 \(T\) 对应的那个点。考虑怎么找这个 \(T\), 我们可以从 \(root\) 不断向上跳, 知道找到一个点的 \(ch_c\) 不为空。我们记这个点为 \(p\)。
在跳的过程中, 把上面的节点的 \(ch_{u, c}\) 变成 \(cyx\)。
则 \(ch_{p, c}\) 为 \(T\) 对应的点。
我们记 \(q = ch_{p, c}\)。
接下来分成两种情况。
第一种 : 若 \(q\) 节点最长的串为 \(T\)。则我们只需要让 \(cyx\) 的父亲为 \(p\)。
第二种 : 若 \(q\) 节点最长的不串为 \(T\), 则我们把 \(q\) 分裂成两个点。\(q1, q\), 我们让原 \(q\) 长度 \(\le |T|\) 的为 \(q1\) 点对应的子串, 其他的为 \(q\)。这时, 我们需要把原来\(ch\)连向 \(p\)的点重新连向 \(q1\)。我们可以证明, 这些点一定在且在 \(p\) 连向根的路径上。由于我们是较长的继承原来的, 所以不需要考虑下面的。
一些sb错误理解(没错就是我)
你也许会想到这样一个过程, 你分裂的时候假设原来的点为 \(q\), 你可不可以这样分裂 : 长度较小的点继承 \(q\)。
这样, 你会被这组数据 hack
abbab
这大概是加入地3个字符时把 \({b, ab}\) 分裂成 \({b}\) 和 \({a, b}\), \(a\) 存在一个 \(ch_{b}\), 所以字符 \(ab\) 也应该有连边。
所有告诉我们只有这个点的所有连边都能通过一种方法匹配才可以这样做。
时间复杂度
建议先看一下代码。接下来的分析和代码中的变量名一致, 和原来的变量名无关。
一个关键事实 : \(dep_{ch_{u, c}} \le dep_{u}\)
证明(非常建议看)
若 \(ch_{u, c}\) 不为 \(0\), 则对于 \(u\) 的每一个祖先 \(v\), \(ch_{v, c}\) 也绝对不为 \(0\), 且 \(ch_{v, c}\) 为 \(ch_{u, c}\) 的祖先。所以最坏情况下就是一个 \(v\) 对应一个不同的 \(ch_{v, c}\), 故 \(dep_{ch_{u, c}} \le dep_{u}\)
对于 for(; p && !t[p].ch[pk]; t[p].ch[pk] = root, p = t[p].fa); 我们考虑 \(root\) 的 \(dep\) (其实这个过程相当于一条路径)。每次想上跳一步, \(root\) 的深度 --, 对于走 \(ch\) 这条边, 运用上面的结论可知, 跳的节点的深度 \(\le\) 当前节点, 最后该节点的深度最多加1, 故时间复杂度为 \(O(n)\)
接下来考虑循环 for(; p && t[p].ch[pk] == q; t[p].ch[pk] = new_node, p = t[p].fa);, 我们发现这个东西其实和 \(dep_{root}\) 有关, 所以我们考虑 \(dep_p\) 与 \(dep_q\) 的关系 (此刻 \(p, q\) 均为没有分裂强的变量) 首先我们知道 \(dep_q \le dep_p\), 若经过一次循环, 则最坏情况下是上面的一一匹配, 所以我们知道 \(dep_q \le dep_p - 1\), 一次类推, 我们知道每次循环我们 \(dep_q\) 的最大可能值都会减少, 我们知道 \(dep_root = dep_q + 1\), 所以说 \(dep_q\) 最大值减小等价于 \(dep_root\) 最大值减小, 最坏情况下取等, 所以时间复杂度为 \(O(n)\)。
Code
int push_back(){
++tot;
t[tot].fa = 0;
for(int i = 0; i < 26; ++i){
t[tot].ch[i] = 0;
}
t[tot].cnt = 0, t[tot].len = 0;
return tot;
}
void Insert(char c){
p = root;
int pk = c - 'a';
root = push_back();
t[root].len = cct;
t[root].cnt = 1;
for(; p && !t[p].ch[pk]; t[p].ch[pk] = root, p = t[p].fa);
if(!p){
t[root].fa = 1;
}
else{
int q = t[p].ch[pk];
if(t[q].len == t[p].len + 1){
t[root].fa = q;
return ;
}
int new_node = push_back();
for(int i = 0; i < 26; ++i)t[new_node].ch[i] = t[q].ch[i];
t[new_node].fa = t[q].fa;
t[new_node].len = t[p].len + 1;
t[q].fa = new_node;
t[root].fa = new_node;
for(; p && t[p].ch[pk] == q; t[p].ch[pk] = new_node, p = t[p].fa);
}
}

浙公网安备 33010602011771号