ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

SAM 基础

2022-06-18 18:37:54  阅读:177  来源: 互联网

标签:子串 状态 SAM 后缀 基础 len endpos 字符串


SAM 的定义

  • SAM 是一张有向无环图。结点被称作 状态 ,边被称作状态间的转移

  • 图存在一个源点 \(t_0\) ,称作 初始状态,其它各结点均可从 \(t_0\) 出发到达

  • 每个 转移 都标有一些字母。从一个结点出发的所有转移均不同

  • 存在一个或多个 终止状态 。如果我们从初始状态 \(t_0\) 出发,最终转移到了一个终止状态,则路径上所有转移连接起来一定是字符串 \(s\) 的一个后缀。\(s\) 的每个后缀均可用一条从 \(t_0\) 到某个终止状态的路径构成

  • 在所有满足上述条件的自动机中,SAM 的结点数最少

子串的性质

  • SAM 包含关于字符串 \(s\) 的所有子串的信息。任意从初始状态 \(t_0\) 开始的路径,如果我们将转移路径上的标号写下来,都会形成 \(s\) 的一个子串。反之,每个 \(s\) 的子串对应从 \(t_0\) 开始的某条路径。

  • 到达某个状态的路径可能不止一条,因此我们说一个状态对应一些字符串的集合,这个集合的每个元素对应这些路径

结束位置 endpos

  • 考虑字符串 \(s\) 的任意非空子串 \(t\) ,我们记 \(endpos(t)\) 为在字符串中 \(t\) 的所有结束位置

  • 两个子串的 \(endpos\) 集合可能相等,这样所有字符串 \(s\) 的非空子串都可以根据它们的 \(endpos\) 集合被分为若干 等价类

  • 对于 SAM 中的每个状态对应一个或多个 \(endpos\) 相同的子串,也就是SAM 中的状态数等于所有子串的等价类的个数,再加上初始状态。SAM 的状态个数等价于 \(endpos\) 相同的一个或多个子串所组成的集合的个数 + 1

一些引理

  • 字符串 \(s\) 的两个非空子串 \(u\) 和 \(w\) (假设 \(|u|\leq|w|\))的 \(endpos\) 相同,当且仅当字符串 \(u\) 在 \(s\) 中的每次出现,都是以 \(w\) 的后缀形式存在的

  • 考虑两个非空子串 \(u\) 和 \(w\) (\(|u|\leq|w|\))。那么要么 \(endpos(u)\) 和 \(endpos(w)\) 不相交,要么 \(endpos(w)\) 是 \(endpos(u)\) 的子集,这取决于 \(u\) 是否是 \(w\) 的一个后缀

  • 如果集合 \(endpos(u)\) 与 \(endpos(w)\) 有至少一个公共元素,那么 \(endpos(w)\) 是 \(endpos(u)\) 的子集

  • 考虑一个 \(endpos\) 等价类,将类中所有子串按长度非递增顺序排序。每个子串都不会比它前一个子串长,且一定是前一个子串的后缀。也就是对于同一等价类的任一两子串,较短者为较长者的后缀,且该等价类中中的子串长度恰好覆盖一个区间

后缀链接 link

  • 考虑一个非初始状态的状态 \(v\) 。我们已经知道状态 \(v\) 对应于具有相同 \(endpos\) 的等价类。我们定义 \(w\) 为这些字符串中最长的一个,则所有其它字符串都是 \(w\) 的后缀

  • 我们还知道字符串 \(w\) 的前几个后缀(按长度降序考虑)全部包含于这个等价类,且所有其它后缀(至少有一个--空后缀)在其它的等价类中,我们记 \(t\) 为最长的这样的后缀,然后将 \(v\) 的后缀链接接连奥 \(t\) 上

  • 换句话说,一个后缀链接 \(link(v)\) 链接到对应 \(w\) 的最长后缀是另一个 \(endpos\) 等价类的状态

  • 以下我们假设初始状态 \(t_0\) 对应它自己这个等价类 (只包含一个空字符串)。为了方便,我们规定 \(endpos(t_0)=\{-1,0,\cdots,|S|-1\}\)

一些引理

  • 所有后缀链接构成一颗根节点为 \(t_0\) 的树

  • 通过 \(endpos\) 集合构造的树(每个子节点的 \(subset\) 都包含在父节点的 \(subset\) 中)与通过后缀链接 \(link\) 构造的树相同

小结

  • \(s\) 的子串可以根据它们结束的位置 \(endpos\) 被划分为多个等价类

  • SAM 由初始状态 \(t_0\) 和与每一个 \(endpos\) 等价类对应的每个状态组成

  • 对于每一个状态 \(v\) ,一个或多个子串与之匹配。我们记 \(longest(v)\) 为其中最长的一个字符串,记 \(len(v)\) 为它的长度。类似地,记 \(shortest(v)\) 为最短的子串,它的长度为 \(minlen(v)\) 。那么对应这个状态的所有字符串都是字符串 \(longest(v)\) 的不同后缀,且所有字符串的长度恰好覆盖区间 \([minlen(v),len(v)]\) 中的每一个整数

  • 对于任意不是 \(t_0\) 以外的状态 \(v\) ,定义后缀链接为连接到对应字符串 \(longest(v)\) 的长度为 \(minlen(v)-1\) 的后缀的一条边。从根节点 \(t_0\) 出发的后缀链接可以形成一棵树。这棵树也表示 \(endpos\) 集合间的包含关系

  • 对于 \(t_0\) 以外的状态 \(v\) ,可用后缀链接 \(link(v)\) 表达 \(minlen(v)\):

    \[minlen(v)=len(link(v))+1 \]

  • 如果我们从任意状态 \(v_0\) 开始顺着后缀链接遍历,总会到达初始状态 \(t_0\) 。这种情况下我们可以得到一个互不相交的区间 \([minlen(v_i),len(v_i)]\) 的序列,且它们的并集形成了连续的区间 \([0,len(v_0)]\)

算法

\(\quad\) SAM 是 在线 算法,我们可以以逐个加入字符串中的每个字符,并且在每一步中对应地维护 SAM 。

\(\quad\)为了保证线性的空间复杂度,我们将只保存 $ len$ 和 \(link\) 的值和每个状态的转移列表,我们不会标记终止状态。

\(\quad\)一开始 SAM 只包含一个状态 \(t_0\) ,编号为 \(0\) (其它状态的编号为 \(1,2,...\))。为了方便,对于状态 \(t_0\) 我们指定 \(len=0,link=-1\) (\(-1\) 表示虚拟状态)

\(\quad\)现在,任务转化为实现给当前字符串添加一个字符 \(c\) 的过程。算法流程如下:

  • 令 \(last\) 为添加字符 \(c\) 之前,整个字符串对应的状态(一开始我们设 \(last\) ,算法的最后一步更新 \(last\)) 。

  • 创建一个新的状态 \(cur\) ,并将 $len(cur) $ 赋值为 \(len(last)+1\) ,在这时 \(link(cur)\) 的值还未知

  • 现在我们按以下流程进行(从状态 \(last\) 开始)。如果还没有到字符 \(c\) 的转移,我们就添加一个到状态 \(cur\) 的转移,遍历后缀链接。如果在某个点已经存在到字符 \(c\) 的转移,我们就停下来,并将这个状态标记为 \(p\)

  • 如果没有找到这样的状态 \(p\) ,我们就到达了虚拟状态 \(-1\) ,我们将 \(link(cur)\) 赋值为 \(0\) 并退出

  • 假设我们找到一个状态 \(p\) ,其可以通过字符 \(c\) 转移。我们将转移到状态标记为 \(q\)

  • 现在我们分类讨论两种状态,要么 \(len(p)+1=len(q)\) ,要么不是

  • 如果 \(len(p)+1=len(q)\) ,我们只要将 \(link(cur)\) 赋值为 \(q\) 并退出

  • 否则我们需要 复制 状态 \(q\) :我们创建一个新的状态 \(clone\) ,复制 \(q\) 的除了 \(len\) 值以外的所有信息(后缀链接和转移)。我们将 \(len(clone)\) 赋值为 \(len(p)+1\)

    复制之后,我们将后缀链接从 \(cur\) 指向 \(clone\) ,也从 \(q\) 指向 \(clone\)

    最终我们需要使用后缀链接从状态 \(p\) 忘回走,只要存在一条通过 \(p\) 到状态 \(q\) 的转移,就将该转移重定向到状态 \(clone\)

  • 以上三种情况,在完成这个过程后,我们将 \(last\) 的值更新为状态 \(cur\)

\(\quad\)如果我们还想知道哪些状态是 终止状态 而哪些不是,我们可以在为字符串 \(s\) 构造完完整整的 SAM 后找到所有的终止状态。为此,我们从对应整个字符串的状态 (存储在变量 \(last\) 中) ,遍历它的后缀链接,直到到达初始状态。我们将所有遍历到的节点都标记为终止节点。容易理解这样做我们会准确地标记字符串 \(s\) 地所有后缀,这些状态都是终止状态。

正确性证明

  • 若一个转移 \((p,q)\) 满足 \(len(p)+1=len(q)\) ,我们称这个转移是 连续地 。否则,即当 \(len(p)+1<len(q)\) 时,这个转移被称为 不连续的 。从算法描述中可以看出,连续的、不连续的转移时算法的不同情况。连续的转移是固定的,我们不会再改变了,与此相反,当向字符串中插入一个新的字符时,不连续的转移可能会改变(转移边的端点可能会改变)。

  • 为了避免引起歧义,我们记向 SAM 中插入当前字符 \(c\) 之前的字符串为 \(S\)

  • 算法从创建一个新状态 \(cur\) 开始,对应于整个字符串 \(s+c\) 。我们创建一个新的节点,与此同时我们也创建了一个新的字符和一个新的等价类

  • 在创建一个新的状态后,我们会从对应整个字符串 \(s\) 的状态通过后缀链接进行遍历。对于每一个状态,我们尝试添加一个通过字符 \(c\) 到新状态 \(cur\) 的转移。然而我们只能添加与原有转移不冲突的转移。因此我们只要找到已存在 \(c\) 的转移,我们就必须停止

  • 最简单的情况是我们到达了虚拟状态 -1 ,这意味着我们为所有 \(s\) 的后缀添加了 \(c\) 的转移,这也意味着,字符 \(c\) 从未在字符串 \(s\) 中出现过。因此 \(cur\) 的后缀链接为状态 0

  • 第二种情况下,我们找到了现有的转移 \((p,q)\) ,这意味着我们尝试向自动机内添加一个已经存在的字符串 \(x+c\) (其中 \(x\) 为 \(s\) 的一个后缀,且字符串 \(x+c\) 已经作为 \(s\) 的一个子串出现过了 )。因为我们假设字符串 \(s\) 的自动机的构造是正确的,我们不应该在这里添加一个新的转移,然而,难点在于从状态 \(cur\) 出发的后缀链接应该连接到哪个状态呢? 我们要把后缀链接接连到一个状态上,且其中最长的字符串恰好是 \(x+c\) ,即这个状态的 \(len\) 是 \(len(p)+1\) ,然而还不存在这样的状态,\(len(q)>len(p)+1\) ,这种情况下,我们必须通过拆开状态 \(q\) 来创建一个这样的状态

  • 如果转移 \((p,q)\) 是连续的,那么 \(len(q)=len(p)+1\) ,这种情况下只需要将 \(cur\) 的后缀链接指向状态 \(q\)

  • 否则状态是不连续的,这意味着状态 \(q\) 不止对应于长度为 \(len(p+1)\) 的后缀 \(s+c\) ,还对应于 \(s\) 更长的子串。除了将状态 \(q\) 拆成两个子状态以外我们别无它法,所以第一个子状态的长度就是 \(len(p)+1\) 了。

    我们如何拆开一个状态呢?我们 复制 状态 \(q\) ,产生一个状态 \(clone\) ,我们将 \(len(clone)\) 赋值为 \(len(p)+1\) ,由于我们不想改变遍历到 \(q\) 的路径,我们将 \(q\) 的所有转移复制到 \(clone\) ,我们也将从 \(clone\) 出发的后缀链接设置为 \(q\) 的后缀链接的目标,并设置 \(q\) 的后缀链接为 \(clone\)

    在拆开状态后,我们将从 \(cur\) 出发的后缀链接设置为 \(clone\)

    最后一步我们将一些到 \(q\) 的转移重定向到 $clone $ 。我们需要哪些修改呢?

    只重定向相当于所有字符串 \(w+c\) (\(w\) 是 \(p\) 的最长字符串)的后缀就够了。即,我们需要继续沿着后缀链接遍历,从结点 \(p\) 直到虚拟状态 \(-1\) 或者转移到不是状态 \(q\) 的一个转移

操作次数为线性的证明(略)

标签:子串,状态,SAM,后缀,基础,len,endpos,字符串
来源: https://www.cnblogs.com/kzos/p/16388923.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有