zoukankan      html  css  js  c++  java
  • 后缀三兄弟$ig( ext{(alpha+1,beta]} 版本 ig)$


    前言

    后缀三兄弟是处理字符串问题的有力工具, 它们之间有共通之处, 即长存不灭的金子般的 idea; 它们之间也可以用算法相互转化。

    后缀数组

    后缀数组是三个数组, 分别名为 (sa)(rank)(height), 它们包含的信息都是同一个字符串的。


    sa数组

    sa 数组的定义
    一个串 (S) 的后缀 (S[l:|S|])(l) 表示, 将 (S) 的所有后缀排序, sa 数组存储的就是有序的后缀编号 (l)

    暴力求 sa
    如果用 C++ 带的 sort 函数来后缀排序的话, 时间复杂度是 (O(n^2 log_2 n)) 的, 因为两个字符串的比较的时间复杂度是 (O(n)) 的。

    快速排序与基数排序
    对于用 k 个关键字排序 n 个数, 快速排序能做到 (O(kn log_2 n)), 基数排序能做到 (O(kn))
    下面简要分析一下它们的排序过程。

    快速排序: 都知道 sort 函数提供了 cmp 函数的接口。
    基数排序: 基数排序是将多个关键字分开, 从低优先级的关键字到高优先级的关键字依次排一遍(稳定排序)。

    倍增法
    倍增法的低复杂度依托于后缀排序这个问题的特殊性质。
    有了 (S[iin[1,n], i+2^{k-1}-1]) 的排序结果, 就可以得到 (S[iin[1,n], i+2^k-1]) 的排序结果。
    每一次倍增的过程中都要用到一次二关键字排序, 用基数排序就可以将一次倍增做到 (O(n))
    倍增法的总复杂度为 (O(nlog_2 n))


    LCP 与 sa数组

    (lcp(i,j)) 表示 (LCP(sa_i, sa_j))

    +
    (Delta delta) 性质(自己瞎取的名, 这个性质不仅适用于后缀排序问题, 它适用于所有多关键字排序问题):对于在 sa 数组中的一个串 (sa_i), 以及在 sa 数组中与其相对位置一样的若干串 (sa_j)(|rk[i]-rk[j]|) 变大, (lcp(i,j)) 不增。
    证明:对于两个串 (A)(B), 它们都有 (n) 个关键字(不足补空), (A_i)(A) 的第 (i) 关键字, (LCP(A,B) = k)(A)(B) 的前 (k) 个关键字都相等, 第 (k+1) 关键字不同。
    由于排序的性质, 对于一个串 (S) 和与其相对位置相同的一些串来说, 排序后第一关键字与 (S) 相同的那些串一定比第一关键字与 (S) 不同的那些串近 (因为第一关键字相同的在整个数组中一定是连续的), 这也就说明了与 (S) 的最长公共前缀为 (0) 的串总是离 (S) 较与 (S) 的最长公共前缀为 (ge 1) 的串远。
    在与 (S)(LCP) 大于等于 (1) 的那个连续段中, 第二关键字就是第一关键字, 用归纳法就可以发现这个 (Delta delta) 性质。

    +
    有了这个 (Delta delta) 性质, 可以证明一个定理:

    [lcp(i,j) = min{ lcp(i,k), lcp(k,j) } ag{$i le k le j$} ]

    证明:
    首先 (lcp(i,k) ge min{ lcp(i,k), lcp(k,j) })(lcp(k,j) ge min{ lcp(i,k), lcp(k,j) }), 由此推出 i、j 前 (min{ lcp(i,k), lcp(k,j) }) 个字符相等, 即 (lcp(i,j) ge min{ lcp(i,k), lcp(k,j) })

    (Delta delta) 性质, (lcp(i,j) le lcp(i,k))(lcp(j,i) le lcp(j,k)), 即 (lcp(i,j) le min{ lcp(i,k), lcp(k,j) })

    综上, (lcp(i,j) = min{ lcp(i,k), lcp(k,j) })

    需要注意的是, 这个定理也适用于任意字符串排序的结果数组, 而不仅仅是后缀排序的结果数组

    这个定理有一个应用:

    [lcp(i,j) = min_{k=i+1}^j{lcp(k,k-1)} ]

    证明:
    (lcp(i,j)) 按照定理展开, 最终就可以得到这个式子。(至于展开的过程中 (k) 怎么选, 当然是随便选啦owo)


    rk 数组

    rk 数组是 sa 数组的反函数, 意即 (sa[rk[i]] = i)


    height 数组

    定义

    [height_i = LCP(sa_i, sa_{i-1}) ]

    意即排名为 i 的后缀与排名为 i-1 的后缀的 LCP。

    又有

    [h_i = height_{rank_i} = LCP(i, sa_{rank_i - 1}) ]

    意即后缀 i 与排在它前面一个的后缀的 LCP。

    这里有一个定理:

    [forall 1<i le n, ; h_i ge h_{i-1} - 1 ]

    证明:

    如果 (h_{i-1} le 1), 那么显然成立。
    如果 (h_{i-1} > 1), 那么将 (i-1)(sa_{rank_{i-1} - 1}) 都去掉第一个字符, 得到 (i)(sa_{rank_{i-1} - 1}+1), 它们的 (LCP) 就是 (h_{i-1}-1); 由字典序的知识可知 (sa_{rank_{i-1} - 1}+1) 依然排在 (i) 前面, 再由 (i) 前面离 (i) 最近的是 (sa_{rank_i - 1})(Deltadelta) 性质, 定理就得证了。

    有了这个定理, 就可以 (O(n)) 求出 (h) 数组, 进而 (height) 数组也就可以求出了。

    以上就是后缀数组的基本知识了。


    「」

    闲话一下: 很久以前看到了有人写边压缩的 Trie (这里), 当时看到评论里有人说这类似后缀树的思想, 当时还被吓到觉得后缀树是个什么东西, 好高端好niubi啊; 现在可以拿出[这篇博文]压惊, 于是我很少被nb玩意吓到了。

    后缀树定义: 将一个串的所有后缀不重不漏地插入一个空 Tire, 就得到了它的后缀树。(的前身)
    小性质: 朴素的后缀树节点个数是 (O(n^2)) 的。 然而由于叶子节点数是 (O(n)) 的, 得出子节点超过一个的节点数也是 (O(n)) 的。(感性理解下: 每插入一个串最多产生一个新的子节点超过一个的节点)

    优化节点个数的思想——虚树:
    对于树 (T = (V,E)), 给定一堆关键点 (S subseteq V), 那么通过这些关键点可以定义这棵树的虚树 (T_0 = (V_0, E_0))。 其中, 节点集合 (V_0 subseteq V), 使得 (u in V_0) 当且仅当 (u in S)(exists x,y in S, u = LCA(x,y))((u,v) in E_0), 当且仅当 (u,v in V_0)(u)(v)(V_0) 中深度最浅的祖先。

    建出虚树, 减小了树的规模, 但同时压缩了树的信息(指保留了一部分信息并丢掉了另一部分信息), 上面那个定义需要细细品味, 或是在题目中加强对它的认识(如果想练习虚树的话)。

    虚树的构建法:
    依据树 (T = (V,E)) 和关键点集 (S subseteq V) 构造虚树 (T_0 = (V_0, E_0)) 的方法如下 :
    首先给 (forall u in V) 标上 dfs序 : (dfn(u))
    然后将 (S) 里的节点按照 dfs序 由小到大排序, 依次取出, 同时用栈维护当前 (T_0) 的 “右链” (也就是所谓树的 “右脊”:根节点到dfn最大的节点的路径构成的一条链):
    设这次取出的点为 (u), 右链中最深的点为 (v)(w = LCA(u,v)), 将 (dep_q > dep_w)(q) 弹栈。 注意, 加边操作总是发生在弹栈的过程中, 具体的实现还要加亿点细节, 在此不再赘述。

    后缀树的线性在线构建——Ukknonen算法:
    Ukkonen算法类似于构建后缀自动机(SAM),采用增量法: 在线构造每次末尾插入一个字符利用旧的后缀树维护出新的后缀树。 -- Magolor

    Ukkonen算法(简称ukk算法)是一个online算法,它与mcc算法的一个显著区别是每次只对S的一个前缀生成隐式后缀树(implicit suffix tree),然后考虑S的下一个字符S[i+1]并将S[0...i+1]的所有后缀加入到上一个阶段中生成的隐式后缀树中,形成一个新的隐式后缀树。最后用一个特殊字符将隐式后缀树自动转换成真实的后缀树。这样ukk的一个最大优点就是不需要事先知道输入字串的全部内容,只需使用增量方式生成后缀树。和mcc算法类似,也是采用压缩存储Trie,以达到节省空间的目的。通过使用implicit extensions和suffix link两大技巧,时间复杂度可以达到线性。 -- ljsspace

    上面两段分别是两篇介绍 Ukkonen算法 的博文的开头奠基部分。写一份真正能用的知识点教程会花费较多的精力, 由于这点,网上找好教程是真的难,如果要学知识点, 建议大家尽量少看高中算法竞赛选手现役时写的博客,很难找到良品。

    真的心态崩了, 能把这么简单的东西讲得跟天书一样

    要学 Ukkonen算法, 看这篇完全足够了, 我会抽空在这里再写一遍。

    一个神必概念和它的衍生物:
    「隐式后缀树」(implicit suffix tree):一个后缀的终止节点可能是叶子结点,也可能是非叶子结点, 这样的后缀树叫做隐式后缀树。

    如果在输入串中最后加一个不同于这个串的其它所有字符的 “终止符”,那么所有的后缀都将终止于叶子结点,不会有后缀隐藏在内部结点中。(因为这个字符不会出现在非叶子节点中, 出现了就表示它在这个串里)


    cls 太强啦

    一个串 (S) 的后缀自动机 (SAM) 是可以且仅可以接受 (S) 的所有后缀的 DFA(有限状态自动机)。
    最简 (SAM) 是指拥有最少状态与转移的 SAM, 我很喜欢最简 SAM, so 以下说到 SAM 都是指最简 SAM。

    几个约定:
    以下讨论 SAM 时, 母串为 (S)(S) 的长度为 (|S|), 下标从 (1) 开始。
    分别用 (fac[l:r])(pre[i])(suf[i]) 表示 (S) 的 “从 (l)(r) 的子串”、 “以 (i) 结束的前缀”、 “从 (i) 开始的后缀”; 分别用 (Fac)(Pre)(Suf) 表示 (S) 的 “所有子串组成的集合”, “所有前缀组成的集合”、 “所有后缀组成的集合”。
    (SAM(string)) 表示 (delta(Start, string))
    对于状态 (u), 令 (Reg(u)) 表示所有使得 (delta(u,str)) 被 SAM 接受的串 (str) 组成的集合。
    对于 (forall str in Fac), 若其在 (S) 中的出现集合为 ({ S[l_1:r_1], cdots,S[l_n,r_n] }), 定义 (Right(str) = {r_1,cdots,r_n })

    SAM 的时空复杂度证明
    显然, (SAM(str) eq Null) 当且仅当 (str in Fac), 但是如果对于 (forall str in Fac) 都建一个单独的状态, SAM 的状态总数就是 (O(|S|^2)) 的了。
    对于 (forall str in Fac), 若 (Right(str) = {r_1,cdots,r_n }), 那么 (str + S[r_i+1:|S|] in Suf), 意即 (right) 集合决定了能够通过往后面加字符串转移到的状态集合, 那么 (right) 集合相等的两个串, 往它们后面加同样的字符, 两个串能否转移到后缀状态是同步的, 它们的转移过程也很相似, 所以可以把这两个串对应的状态合并成一个, 相关的后续状态也合并成一个, 那么 SAM 中的一个状态就代表一个 “right等价类”。

    接下来考虑 (S) 的两个不同的子串 (A)(B), 如果 (right(A) cap right(B) eq Null), 那么显然其中一个是另一个的后缀, 不妨设 (A)(B) 的后缀, 此时显然 (right(B) subseteq right(A))。 所以对于 (S) 的两个子串, 它们的 (right) 集合要么没有交集, 要么一个包含另一个。
    令一个状态 (u) 的父状态 ((fa_u)) 为满足 (right(u) subseteq right((fa_u)))(|right((fa_u))|) 最小的状态, 这时候所有的状态会形成一个树结构, 把它叫做 (parent) 树。 SAM 的 (parent) 树至多只有 (O(|S|)) 个叶子结点(叶子结点就是 (|right| = 1) 的节点, 由于每个节点代表的都是一个 (right) 等价类, 又因为不同的大小为 1 的等价类个数是 (O(|S|)) 的), 而对于非叶子结点, 如果其只有一个子节点, 那么就可以把它和它的子节点合并, 故每个非叶子结点的子节点多于 1 个, 所以 SAM 的状态数就是 (O(|S|)) 的(具体证明跟后缀树的差不多)。

    考虑转移。注意到状态数是 (O(|S|)) 的, 考虑 SAM 的一个从初始状态开始的树形图, 这个树形图的树边数显然是 (O(|S|)) 的, 只需要考虑非树边。
    对于非树边 (delta(a,c) = b), 构造: 根到 a 的路径 + (a->b) + b 到任意一个接受状态的集合。 所以一个非树边对应着至少一个原串的后缀。对于一个后缀,沿着自动机走,使其对应其路径上经过的第一条非树边, 这是一个后缀对应了一个非树边, 而每个非树边至少被一个后缀所对应, 那么非树边的数量也就是 (O(|S|)) 的了。
    所以 SAM 的转移树也是 (O(|S|)) 的。

    构造法
    后缀自动机的构造方法是增量法。

    "中心法则"

    后缀结构的基本总结

    经典问题选讲(待更)

    更多字符串理论(待更)

  • 相关阅读:
    第二十章 数据访问(In .net4.5) 之 使用LINQ
    第十九章 数据访问(In .net4.5) 之 处理数据
    第十八章 数据访问(In .net4.5) 之 I/O操作
    第十七章 调试及安全性(In .net4.5) 之 程序诊断
    大叔学Spring Boot笔记(14)A component required a bean of type '...Mapper' that could not be found问题解决
    大叔学Spring Boot笔记(13)Free Mybatis plugin使用
    MySQL查询结果中Duration Time和Fetch Time的区别
    大叔学Spring Boot笔记(12)Windows环境下使用bat启动和停止Java【转】
    大叔学Spring Boot笔记(11)jdk/bin目录下的不同exe文件的用途及区别【转】
    大叔学Spring Boot笔记(十)手动编译、打包并运行项目
  • 原文地址:https://www.cnblogs.com/tztqwq/p/13409778.html
Copyright © 2011-2022 走看看