zoukankan      html  css  js  c++  java
  • 后缀数据结构学习笔记

    后缀数据结构学习笔记

    By Tuifei_oier

    后缀数据结构通常应用于字符串,用于求解一些字符串相关的问题,也因此这种数据结构往往可以和字符串捆绑在一起和其他算法嵌套,所以它的应用也是较繁杂的,但是也有一定的定式。

    Part 1 基本定义

    1. 字符串:在 OI 中表示为一些字符构成的数组,默认下标从 (1) 开始(n) 表示其长度。
    2. 子串:一个字符串中的连续一段,从下标 (l) 到下标 (r)(S) 的子串记为 (S[l,r])
    3. 前缀:下标 (i) 的前缀即子串 (S[1,i])
    4. 后缀:下标 (i) 的后缀即子串 (S[i,n])

    Part 2 后缀数组

    定义&构造

    先来看一个题目:

    给定字符串 (S),求它的所有后缀按字典序排序的结果。
    (tips:1le nle10^6)

    我们首先有一个朴素解法:取出 (S) 的所有后缀,然后直接排序即可,复杂度 (O(n^2)),显然有点大了。
    于是,我们考虑先取出它的所有后缀,考虑我们复杂度的瓶颈:
    因为我们每次比较都是比较两个后缀的下一个字符,所以比较的复杂度为 (O(n^2)),但是注意到后缀之间是有包含关系的,也就是说这种比较方式导致我们有可以利用的信息浪费了,所以考虑倍增优化来利用信息。
    具体而言,每次比较每个后缀的前 (l) 个字符(不足在后面补空字符),然后接下来比较前 (2l) 个字符,我们发现前 (2l) 个字符的比较过程,可以拆成先比较前 (l) 个,再比较后 (l) 个,然后这两个东西的排名在比较前 (l) 个字符的过程中就求出来了,所以就可以直接做一个双关键字排序,这个东西可以用基数排序方便的完成,复杂度为 (O(nlogn))。(排序 (O(n)),排 (O(logn)) 次。)
    之后我们可以得到 (S) 所有后缀排序后的结果,记 (sa[i]) 表示排名为 (i) 的后缀在 (S) 中的起始下标,则 (sa) 数组即为我们所求得的后缀数组。
    至此,我们得到一个 (O(nlogn)) 构造后缀数组的算法。

    应用

    那么,这玩意儿有什么用呢?
    实话实说,没啥用。
    但是,我们通过它再去求一些其它的玩意儿,就相当有用了。
    首先,求一个也没啥用的东西:(rk[i]),它表示后缀 (S[i:n]) 在所有后缀中的排名。显然 (rk[sa[i]] = i,sa[rk[i]] = i)
    然后,我们考虑求这样一个数组 (height[i]),它表示所有后缀中排名为 (i) 的后缀和排名为 (i-1) 的后缀的最长公共前缀(即 LCP)。
    (height[i]=LCP(S[sa[i-1],n],S[sa[i],n]))
    然后,这个东西有什么用呢?我们可以 (O(1)) 求任意两个后缀之间的 (LCP)!

    定理:(LCP(S[i,n],S[j,n])=minlimits_{k=min(rk[i],rk[j])+1}^{max(rk[i],rk[j])}height[k])

    怎么证明?自证不难(我有点懒(不算很难,较好理解))。
    所以,我们只要求出 (height[i]),然后用数据结构维护区间最值即可。
    接下来考虑怎么求 (height[i])
    设一个数组 (h[i]) ,表示 (LCP(S[i,n],S[sa[rk[i]-1],n]))
    那么我们就可以推出一个定理。

    定理:(h[i]ge h[i-1]-1)

    这个定理的证明有很多方式,而且画图后比较好解决,如果实在证明不出来也可以直接 BFS。
    然后只要直接用这个定理暴力求即可,每次暴力匹配的开始长度为上一次匹配的答案 (-1),不难证明复杂度仍为 (O(n))

    接下来是一些例题,用到了后缀数组以及一些它的性质。

    Pro A (Luogu P2408)

    题意:给定字符串 (S),求 (S) 的本质不同的子串个数。
    (tips:1le nle10^6)

    这个题目算是后缀数组的基础应用了,下面讲一种解法。
    首先,因为每个子串都可以唯一地对应一个后缀的前缀,所以只要考虑所有后缀的本质不同的前缀数量。
    这个过程可以这样考虑:根据之前的 (height) 数组的定理,可以发现在排序后的所有后缀中距离越远 (LCP) 越短,所以考虑每个后缀对答案的贡献时,比它排名恰好小 (1) 的后缀是和它重复最多的,所以减去它们的 (LCP) 即可。

    因此,本题的答案即为 (sumlimits_{i=1}^nn-sa[i]+1-height[i])
    时间复杂度 (O(nlogn))

    Pro B (Luogu P4094)

    题意:给定字符串 (S)(Q) 次询问,每次询问给出 (4) 个正整数 (a,b,c,d),求 (S[a,b]) 的所有子串与 (S[c,d])(LCP) 的最大值。
    (tips:1le n,Qle10^5)

    首先利用 子串=后缀的前缀 转化为求 (S[a,b]) 所有后缀与 (S[c,d])(LCP) 最大值。
    因为这个查询的操作不是在排序后的排名上连续的,所以不能直接利用之前的 (height) 的定理,而注意到答案有单调性,考虑二分答案。
    二分这个最大值 (Len),然后就只要确定是否存在解。
    我们可以先求出满足 (LCP(S[i,n],S[c,n])ge Len)(i) 在排序后排名的范围,这个可以通过二分来解决(利用 (height) 的定理),设为 ([L,R]),然后只要求有多少个数对 ((i,rk[i])) 满足 (ale ile b-Len+1,Lle rk[i]le R),经典二维数点问题。

    时间复杂度为 (O(nlog^2n))

    Pro C (Luogu P1117)

    题意:给定字符串 (S),求 (S) 的每一个子串表示成 (AABB) 形式的方案数之和。(要求 (A,B) 为非空串)
    (tips:1le nle30000),多组数据

    首先,我们考虑一个 (AA) 串对答案的贡献。假设从 (i) 开始的 (AA) 串数量为 (a),以 (i-1) 结束的 (AA) 串数量为 (b),则 (i) 这个位置对答案有 (acdot b) 的贡献,并且这样算不会出现重复和遗漏。
    接下来只要考虑怎么统计以某个位置 开始/结束 的 (AA) 串的数量。
    此时我们有一个常用的做法:插分隔符(一般不用实际插入字符)。
    具体而言,为了统计长度为 (2cdot len)(AA) 串,我们在字符串上取下标分别为 (len,2len,3len,...) 的字符并打上标记。然后发现一个长度为 (2cdot len)(AA) 串必然经过且只经过两个相邻标记节点,所以只需统计两个相邻节点之间的贡献。
    假设现在考虑 (i)(i+len) 两个节点的贡献,我们只需对 (S[i:n],S[i+len:n]) 求 LCP,对 (S[1:i-1],S[1:i+len-1]) 求最长公共后缀,设这两个值分别为 (l1,l2)。当且仅当 (l1+l2ge len) 时,这两个标记点才对答案有影响(否则,(AA) 串的长度必然 (<2cdot len),之前统计过了)。不难发现需要维护区间加,差分一下最后还原即可。
    复杂度 (O(nlogn))

    Part 3 后缀自动机

    后缀自动机是除了后缀数组以外另一大后缀数据结构(后缀树好像用处不蛮大?也没见到只能用后缀树做的),因此掌握这种数据结构也是很重要的。

    定义与性质

    后缀自动机由一些节点和边组成,每个节点代表一个状态(这也是 OI 中常用自动机的通式?)。在后缀自动机中,每个节点代表一些原串的子串,满足它们的在原串中的出现位置(即每次出现结束位置的下标)都相同。

    例如:对于 (S=abab),子串 (ab)(b) 的出现位置都相同,为 ({2,4})。则 SAM 中 (ab,b) 会由同一个节点表示。

    这是点的定义,不难发现一个节点包含的所有字符串长度都不相等,长度每次 (-1),且短的串必定是长的串的后缀。

    例如,存在于同一个节点的字符串一定是这种形式:({aabaa,abaa,baa,aa}),不可能是 ({aabaa,baa}) 或者 ({aabaa,aabab})

    上面的定义及推论都是不难理解的。于是我们对一个点可以记录这些信息:它代表的串中的最长串长度,最短串长度,分别记为 (maxl,minl)
    接下来考虑边。
    SAM 中,边分两种:组成 DAG 的转移边和组成 parent 树的父亲边。
    转移边表示 (u) 代表的所有子串后面加上这条转移边上的字符可以得到 (v) 中的子串,而父亲边表示 (u) 代表串的出现位置是 (v) 的子集。
    具体而言:

    1. 节点 (u) 向节点 (v) 连 DAG 边当且仅当该节点代表所有字符串在原串中的下一位都为字符 (c)
    2. 节点 (u) 向节点 (v) 连父亲边当且仅当 (minl_u=maxl_v+1)

    例如,节点 (u) 代表 ({abaa,baa}),节点 (v) 代表 ({aa,a}),节点 (c) 代表 ({abaac,baac}),则 (u)(v) 连父亲边,向 (c) 连一条带着字符‘c’的转移边。

    由此,我们就可以得到 SAM 的定义了。这之后是由它的定义可以得到的一系列性质:

    1. SAM 中的某一个节点 (u) 的所有祖先 (v) 都满足 (v) 中串是 (u) 中串的后缀,并且从 (u) 开始沿着 parent 树(即父亲边)向上一定会走到初始节点,这个过程会访问到 (u) 中最长子串的所有后缀。
    2. SAM 中每个节点存储的子串数量 = (maxl_u-maxl_{fa_u})
    3. SAM 中每个节点代表子串的出现次数 = (sz_u)

    接下来是 SAM 的构建。
    考虑用增量法来构建 SAM,每次加入一个新字符。
    构建过程自行 BFS,结合以上性质就比较好理解了。
    由构建过程可以得到 SAM 点数 (le 2n-1),边数 (le 3n-4)

    应用

    SAM 有一大堆基础应用。

    1. 判断 (T) 是否在 (S) 中出现。
      只需从初始节点开始沿着转移边一直走即可。
    2. 不同子串个数。
      SAM 上每一条 DAG 上的路径都对应一个子串,所以就是经典的 DAG 上 DP。
      还有一种求法:(sumlimits_{i=1}^{tot}maxl_i-maxl_{fa_i})(应用之前的性质)。
    3. 所有不同子串总长度。
      一样的 DP,和上面的 DAG 上 DP 差不多。
      或者考虑每个节点的贡献:

    [sum_{i=1}^{tot}dfrac{maxl_i(maxl_i+1)-maxl_{fa_i}(maxl_{fa_i}+1)}{2} ]

    1. 最小循环移位。
      建出 (S+S) 的 SAM,在上面贪心地挑最小边走 (n) 步即可。
    2. 子串出现次数。
      dfs 预处理出每个节点所代表集合的出现位置集合大小,然后直接在 DAG 上从初始节点开始走即可。
    3. (T)(S) 中第一次出现的位置。
      考虑对每个节点预处理出一个信息 (firstpos_i),表示这个节点代表的所有串出现位置的最小值。
      然后答案即为 (firstpos_u-|T|+1)
      考虑怎么求这个:

    [firstpos_u=egin{cases} maxl_u(u is a new node)\ firstpos_v(u is cloned from v) end{cases}]

    1. 最短的未出现子串。
      同样在 DAG 上 DP,设 (dp_u) 表示 (u) 节点开始最短的没有出现的子串,则答案为 (dp_{root})
      考虑它的求法:

    [dp_u=egin{cases}minlimits_{(u,v)in E}{dp_v}+1(out\_degree e0)\1(out\_degree=0)end{cases} ]

    1. (S,T) 的最长公共子串。
      (S) 构造 SAM,相当于求 (T) 中所有前缀在 (S) 中的最长公共后缀。
      通过指针在 SAM 上游走来求,设当前走到的节点为 (u),最长长度为 (l),则要求 (max{l})
      具体步骤如下:
      目前已经匹配到 (T) 的第 (i) 个字符,考虑匹配第 (i+1) 个。
      a. 如果存在下一个字符的转移,则 (u) 转移到 (v)(下一个状态),同时 (++l)
      b. 如果不存在,(u=fa_u,l=maxl_{fa_u}),继续如上过程直到存在转移。

    Pro A (SPOJ 1812)

    给定 (k) 个字符串 (S_{1,..,k}),求它们的最长公共子串。
    (tips:1le kle10,1le nle10^5)

    直接对第一个串建出 SAM,然后把其他的串放进去按两个字符串的方式跑,这个过程中实时更新每个点的答案,最后求最大即可。
    时间复杂度 (O(kn))

    Summary

    后缀数据结构在字符串中算是比较有套路可循的题,通常在确定应该维护什么后还是比较模板的。所以在今后的练习中一些基本的套路是需要积累才行的。

  • 相关阅读:
    Mysql中use filesort的误区
    Windows双系统
    Java visualvm
    软件设计师06-数据结构
    安装VMware14可能出现的问题
    计算机硬件系统
    Web漏洞扫描
    crunch制作字典
    kali之HexorBase数据库破解
    memcahce 介绍以及安装以及扩展的安装
  • 原文地址:https://www.cnblogs.com/tuifei-oiers-home/p/14226115.html
Copyright © 2011-2022 走看看