后缀自动机入门

后缀自动机入门

符号约定

  1. \(|S|\) 表示字符串 \(S\) 的长度。
  2. 后缀用 \(\mathrm{Suf}(S,i=\mathrm{Any})\) 表示,如果不写\(i\)则表示后缀集合。
  3. \(\Sigma\) 表示字符集大小

构建

endpos

对于一个字符串 \(S\) ,对于任意一个 \(p|S\) ,定义一个集合 \(\mathrm{endpos}(p)\)(以下简称 \(\mathrm{edp}(p)\) ),表示这个子串在 \(S\) 中所有出现的位置,用 \(p\) 中最后一个字母的位置进行记录。

比如 \(S="aabab"\) 中,\(\mathrm{edp}("a")=\{1,2,4\}, \mathrm{edp}("ab")=\{3,5\}\)

我们将 \(\mathrm{edp}\) 相同的子串称为一个类。

引理 1

\[\mathrm{edp}(S_1)\subset \mathrm{edp}(S_2)\Longleftrightarrow S_2\in\mathrm{Suf}(S_1) \]

这个引理比较显然,就不证明了

引理 2

\[\mathrm{edp}(S_1)\wedge \mathrm{edp}(S_2)\ne\empty ,|S_1|<|S_2| \Longrightarrow \mathrm{edp}(S_2)\subset \mathrm{edp}(S_1),S_1\in\mathrm{Suf}(S_2) \]

如果较小的字符串不是较大的字符串的后缀,那么这两个字符串中至少有一个位置的字符不相同,但是这两个不同字符出现在相同的原串位置,显然有问题,因此 \(S_1\) 一定是 \(S_2\) 的后缀,再根据 \(引理1\) ,可以推出 \(\mathrm{edp}(S_2)\subset \mathrm{edp}(S_1)\)

可以发现,两个 \(\mathrm{edp}\) 要么为包含关系,要么没有交。

引理 3

一个类中的子串按长度从小到大排,长度一定连续。并且所有子串,除去最大的子串,都是最大子串的后缀。

根据 \(引理1\) ,容易证明第二句话。

设该类中最长,最短的子串分别为 \(S_{max},S_{min}\)

根据 \(引理2\) ,对于所有 \(S\in\mathrm{Suf}(S_{max}),|S_{min}|<|S|<|S_{max}|\) ,可以发现 \(\mathrm{edp}(S_{max})\subset\mathrm{edp}(S)\subset\mathrm{edp}(S_{min})\)

因为 \(\mathrm{edp}(S_{min})=\mathrm{edp}(S_{max})\),那么 \(\mathrm{edp}(S_{max})=\mathrm{edp}(S)=\mathrm{edp}(S_{min})\)

所以对于所有满足条件的 \(S\) ,都一定在这个类中。

Parent Tree

根据 \(\mathrm{edp}\) 的定义和性质(只有包含和不交),可以根据其包含关系构建出一棵树,一个节点表示一个类,一个节点和任意一个儿子的连接,表示一个包含关系。根节点是空子串,显然它的 \(\mathrm{edp}=[1,|S|]\)

这棵树能够表示所有子串。

这棵树上的节点集合就是 \(\mathrm{SAM}\) 上的节点集合。

我们用 \(\mathrm{fa}(u)\) 表示 \(\mathrm{Parent~~Tree}\) 中节点 \(u\) 的父亲,用 \(\mathrm{longest}(u)\) 表示节点 \(u\) 能够代表的最长的字符串,用 \(\mathrm{mxl}(u)\) 表示 \(|\mathrm{longest}(u)|\)

显然一个节点代表的所有字符串长度不会重复,且从小到大排序,恰好是 \(\mathrm{mxl}(\mathrm{fa}(u))+1~\mathrm{mxl}(u)\),所以一个节点代表的字符串一共有 \(\mathrm{mxl}(u)-\mathrm{mxl}(\mathrm{fa}(u))\)

可以发现,\(\mathrm{Parent~~Tree}\) 中,从父亲节点走向儿子节点,可以表示一个字符串在开头添加字符串。一个节点的所有祖先所代表的字符串,都是这个节点的后缀(废话)

转移边

\(\mathrm{Parent~~Tree}\) 的基础上,我们对每一个节点设立转移边,规模为 \(O(\Sigma)\) ,若一个节点 \(u\) 通过代表字符 \(c\) 转移边到达另一个节点 \(v\) ,则表示 \(u\) 中某一个字符串在末尾加上 \(c\) 就可以变成 \(v\) 中的字符串。(这一点可以联系 \(\mathrm{AC}自动机\) 思考)

终于开始构建

首先确定方案:动态加入每一个字符,同时更新后缀自动机。

用原串表示加入新字符之前的字符串,用新串表示加入新字符之后的字符串。

考虑加入一个字符 \(c\) ,实际上就是将所有原串的后缀都加上 \(c\) ,也就是说所有原串的后缀都可以使用转移边转移到新串的后缀。

现在给出模版代码,慢慢解释:

struct SAMNode { 
    int ch[27]/*转移边*/, len/*上面的mxl*/, fa/*Parent Tree中的父亲*/; 
} nd[maxn];
//lst:原串最长的后缀(原串自己)所在的节点
void SAM_init() {
    lst = nd_cnt = 1, nd[1].len = 0;
}
void SAM_insert(int c) {
    int p = lst, np = lst = ++nd_cnt;
    nd[np].len = nd[p].len + 1;
    for (; p && !nd[p].ch[c]; p = nd[p].fa) nd[p].ch[c] = np;
    if (!p) nd[np].fa = 1;
    else {
        int v = nd[p].ch[c];
        if (nd[v].len == nd[p].len + 1) nd[np].fa = v;
        else {
            int nv = ++nd_cnt;
            nd[nv] = nd[v], nd[nv].len = nd[p].len + 1;
            nd[v].fa = nd[np].fa = nv;
            for (; p && nd[p].ch[c] == v; p = nd[p].fa) nd[p].ch[c] = nv;
        }
    }
}

SAM_init()不讲了,大家都懂。

np表示新加入的节点。

nd[np].len = nd[p].len + 1;
for (; p && !nd[p].ch[c]; p = nd[p].fa) nd[p].ch[c] = np;

首先就是总长度+1。

接着是一个有点意思的循环。第一个循环条件使p只能到达根节点(废话)。接着第二个限制条件,如果节点nd[p]中没有字符串能转移到nd[np],增加转移边。如果找到一个节点存在转移边 \(c\) ,就说明已经存在一个新串的后缀,那么比这个后缀长度小的后缀都存在了,退出。(注意,这里找到的时候原串存在的最长的新串后缀去除新加入的字符 \(c\)

if (!p) nd[np].fa = 1;

若一直跳到根节点,表示原串中没有任何子串是新串的后缀,那么新加入的节点就代表的所有新串的后缀,然后将这个节点设成根节点的儿子。

else {
    int v = nd[p].ch[c];
    if (nd[v].len == nd[p].len + 1) nd[np].fa = v;

如果存在新串的后缀,而且这个最长的已存在后缀所在的节点就只包含这个已存在后缀,假设这个节点的 \(\mathrm{edp}\)\(E\) ,那么在原串中不存在的所有后缀的 \(\mathrm{edp}\) 都是 \(E\) 的子集,没有任何问题,直接将所有不存在后缀所在的类(这个类中 \(\mathrm{edp=\{nd[np].len\}}\) ,因为是新出现的后缀)设成\(v\)的儿子。

else {
    int nv = ++nd_cnt;
    nd[nv] = nd[v], nd[nv].len = nd[p].len + 1;
    nd[v].fa = nd[np].fa = nv;
    for (; p && nd[p].ch[c] == v; p = nd[p].fa) nd[p].ch[c] = nv;
}

如果 \(v\) 中不仅包含最长的已存在后缀,而且包含其他奇奇怪怪的子串(最长已存在子串是他们任何一个的后缀,但他们不是原串中不存在的新串后缀的后缀),那么不能将 \(np\) 直接设成 \(v\) 的儿子。

这时候将 \(v\) 分成两部分,一部分只包含最长已存在后缀,设为 \(nv\) ,另一部分包含奇奇怪怪的子串,保留在 \(v\) 中,显然此时 \(v\) 的父亲应该是 \(nv\) ,而且 \(np\) 的父亲也应该是 \(nv\)

接着将原本转移到\(v\)的代表\(c\)的转移边都放到\(nv\)

复杂度

根据构建过程,可以发现节点规模是 \(O(n)\) 的,若将 \(\Sigma\) 看成常数,那么转移边的数量也是 \(O(n)\)

小总结

后缀自动机中每个节点包含了出现位置相同完全的一类字符串,而且一个节点中的字符串互为后缀。转移边表示一个节点中的某一个字符串添加一个字符可以成为另一个节点中的某一个字符串。\(\mathrm{Parent~~Tree}\) 中从父亲走向儿子,实际上表示在在一类字符串中,在前头加入若干个字符。

简单应用

不相同的子串个数

idea 1

由于 \(\mathrm{SAM}\) 是一个 \(\mathrm{DAG}\) ,而且由于其每个节点代表的字符串集合不会交,所有可以考虑一下dp。设 \(f(u)\) 表示从节点 \(u\) 出发进行转移可以得到的字符串总数(包括空串)。
显然可以得到

\[f(u)=1 + \sum\limits_{v\in ch_u} f(v) \]

代码就不写了。

idea 2

首先每一个节点代表的字符串绝对和其他节点的字符串没有一个是相同的,而每一个节点代表的不相同的字符串共有 \(\mathrm{mxl}(u)-\mathrm{mxl}(\mathrm{fa}(u))\) 个。

所以最终的答案为:

\[\sum\limits_{u\in V} (\mathrm{mxl}(u)-\mathrm{mxl}(\mathrm{fa}(u))) \]

某一个字符串的出现次数

首先一个字符串\(S\)的出现次数其实就是 \(|\mathrm{edp}(S)|\),所以问题转化为求出每一个节点的 \(\mathrm{edp}\)

可以在 \(\mathrm{Parent~~Tree}\) 上做树形dp。
设节点 \(u\)\(\mathrm{edp}\)\(f(u)\)
首先原串中每一个前缀所在的节点 \(u\) 都设为 \(f(u)=1\) ,接着进行转移。

\[f(\mathrm{fa}(u))+=f(u) \]

原文地址:https://www.cnblogs.com/juruohjr/p/15699611.html