首页 > 其他分享 >后缀自动机

后缀自动机

时间:2023-07-13 21:35:26浏览次数:44  
标签:状态 SAM 后缀 endpos 字符串 自动机 转移

自动机入门——后缀自动机

数据结构简介

后缀自动机是一个可以解决许多字符串相关问题的有力的数据结构,字符串的 SAM 可以理解为给定字符串的所有子串的压缩形式,SAM 的空间复杂度和构造的时间复杂度均为线性的,准确的说,一个 SAM 最多有 \(2n-1\) 个节点和 \(3n-4\) 条转移边。

定义

字符串 \(s\) 的 SAM 是一个接受 \(s\) 的所有后缀的最小 DFA(确定性有限自动机或确定性有限状态自动机)。

换句话说:

  • SAM 是一张有向无环图。结点被称作 状态,边被称作状态间的 转移
  • 图存在一个源点 \(t_0\),称作 初始状态,其它各结点均可从 \(t_0\) 出发到达。
  • 每个 转移 都标有一些字母。从一个结点出发的所有转移均 不同
  • 存在一个或多个 终止状态。如果我们从初始状态 \(t_0\) 出发,最终转移到了一个终止状态,则路径上的所有转移连接起来一定是字符串 \(s\) 的一个后缀。\(s\) 的每个后缀均可用一条从 \(t_0\) 到某个终止状态的路径构成。
  • 在所有满足上述条件的自动机中,SAM 的结点数是最少的。

性质

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

为了简化表达,我们称子串对应一条路径,反过来,一条路径也可以对应一个子串。

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

构造

结束位置

结束位置 \(endpos\) 是一个比较重要的概念。

考虑字符串 \(s\) 的任意非空子串 \(t\) ,我们记 \(endpos(t)\) 为在字符串 \(s\) 中 \(t\) 出现的所有位置(用右端点的结束位置来代表),例如串 \(\text{abbaabab}\) 中 \(\text{ab}\) 的结束位置为 \(1,5,7\)。两个子串的 \(t_1\),\(t_2\) 的 \(endpos\) 集合可能相等,这种二元关系显然是等价关系。我们可以根据它们的 \(endpos\) 集合把所有子串划分为若干等价类。

SAM 中的每一个状态都对应一个等价类,也就是说 SAM 的状态总数为等价类的个数 \(+1\)(初始节点)。

  • 引理 \(1\)

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

    证明:引理显然成立。

  • 引理 \(2\)

    考虑两个非空子串 \(u,w\) (假设 \(|u|\le |w|\)),那么要么 \(endpos(u)\cap endpos(w)=\varnothing\) ,要么 \(endpos(w)\subseteq endpos(u)\) ,这取决与 \(u\) 是否为 \(w\) 的一个后缀,如果不是,就是前者,否则就是后者。

    证明:其实也比较显然,因为如果不是后缀,显然 \(w\) 出现的地方 \(u\) 不可能出现,所以是空集,如果是后缀,那么长度小的有可能出现在更多地方,并且一定在 \(w\) 都出现的地方出现过。

  • 引理 \(3\)

    考虑一个 \(endpos\) 等价类,对于同一等价类中的任意两个子串,较短者为较长者的后缀,且该等价类中的子串长度是连续的。

    证明:前面这个后缀关系是显然的,我们来证明它们是连续的。如果不连续,那么设字符串 \(q\) 为夹在两个属于同一等价类的字符串 \(s,t(|s|<|t|)\) 之间的一个字符串,且 \(q\) 为 \(t\) 的后缀,\(s\) 是 \(q\) 的后缀,根据引理 \(2\) ,不难推出矛盾。

通过 SAM 的转移,即一些有向边,通过不同的方式走到状态 \(u\) ,我们就可以得到状态 \(u\) 对应的等价类所对应的所有字符串。

考虑 SAM 中某一个不是 \(t_0\) 的状态 \(v\) ,我们已经知道,状态 \(v\) 对应于具有相同 \(endpos\) 的等价类,设 \(w\) 是最长的一个,那么所有等价类中的字符串都是 \(w\) 的后缀。

我们还知道字符串 \(w\) 的前几个后缀全部包含于这个等价类,且所有其它后缀都在其他的等价类中,我们记 \(t\) 为最长的一个后缀,包含 \(t\) 的等价类不是 \(v\)。然后将 \(v\) 的后缀链接连到 \(t\) 的等价类所代表的状态上。

为了方便,我们规定:\(endpos(t_0)=\{-1,0,...,|s|-1\}\) 。

  • 引理 \(4\)

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

    比较显然,首先一定有 \(n-1\) 条边,其次因为字符串长度递减,所以不会出现环。然后一直递减,一定会到达初始状态 \(t_0\) 。

  • 引理 \(5\)

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

    由引理 \(2\) ,这种实质是后缀关系的 \(endpos\) 能够形成一棵树。我们考虑不是 \(t_0\) 的状态 \(v\) ,显然有 \(endpos(v)\subsetneq endpos(link(v))\)。所以定理成立。

    但是需要注意的是,这棵树上,儿子节点的 \(endpos\) 集合不一定是父亲节点的一个划分,反例就是父亲节点的状态包含原字符串上的一个前缀。如果不包含前缀的话,性质是成立的。

小结

  • \(s\) 的子串可以被划分成多个等价类。
  • SAM 由若干状态构成,其中每一个状态对应一个等价类。对于每一个状态 \(v\) ,一个或多个子串与之匹配,我们记 \(longest(v)\) 为里面最长的一个,记 \(len(v)\) 为它的长度,记 \(shortest(v)\) 为最短的子串,它的长度为 \(minlen(v)\) ,那么所有字符串的长度恰好覆盖 \([minlen(v),len(v)]\) 中的每一个整数。
  • 后缀链接可以定义为连接到对应某个状态满足 \(longest(v)\) 的长度为 \(minlen(u)-1\) 且是后缀关系的一条从 \(u\) 到 \(v\) 的边。后缀链接形成了一棵以 \(t_0\) 为根节点的内向树。这棵树也表示 \(endpos\) 集合间的包含关系。
  • 我们有 \(minlen(v)=len(link(v))+1\)
  • 如果我们从 \(v_0\) 开始一直走到 \(t_0\) ,那么沿途所有字符串的长度形成了连续的区间 \([0,len(v_0)]\)。

算法

这个算法是一个在线算法,可以逐个加入字符串中的每个字符并在每一步维护 SAM。

一开始 SAM 只包含一个状态 \(t_0\) ,编号为 \(0\) ,对于状态 \(t_0\) 我们指定 \(len=0,link=-1\) 。(这里 \(-1\) 就是一个虚拟状态)

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

  • 令 \(last\) 为添加字符 \(c\) 之前,整个字符串对应的状态。
  • 创建一个新的状态 \(cur\) ,并将 \(len(cur)\) 赋值为 \(len(last)+1\) 。
  • 现在我们从状态 \(last\) 开始按以下流程进行:如果没有字符 \(c\) 的转移,我们就添加一个到状态 \(cur\) 的转移,遍历后缀链接,如果在某个点已经存在字符 \(c\) 的转移,我们就停下来,并将这个状态标记为 \(p\) 。
  • 如果没有找到这样的状态 \(p\) ,我们就到达了虚拟状态 \(-1\) ,我们将 \(link(cur)\) 赋值为 \(0\) 并退出。
  • 假设现在我们找到了一个状态 \(p\) ,其可以通过字符 \(c\) 转移,我们将转移到的状态记为 \(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\)。

因为我们只对 \(s\) 的每一个字符建立一个或两个新状态,所以 SAM 只包括线性个状态。

正确性证明

  • 如果一个转移 \((p,q)\) 满足 \(len(p)+1=len(q)\) ,则我们称这个转移是连续的。否则,即当 \(len(p)+1<len(q)\) 时称其为不连续的。连续的转移是固定的,而不连续的转移可能会改变。

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

  • 算法从创建一个新状态 \(cur\) 开始,对应于整个字符串 \(s+c\) ,我们创建一个新的节点的原因很清楚,就是要创建一个包含 \(endpos(s+c)\) 的等价类。

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

  • 换句话说,当我们加入一个字符 \(c\) 的时候,会产生 \(|s|\) 个新的后缀,我们不断跳后缀链接,其实就是不断跳 \(s\) 的后缀,然后如果不冲突我们就连一条到 \(cur\) 的边。

  • 如果不存在冲突,也就是说我们到达了虚拟状态 \(-1\) ,那意味着我们为所有 \(s\) 的后缀所对应的状态添加了转移 \(c\) ,这同时也意味着 \(c\) 之前从来没有在字符串中出现过,所以显然 \(cur\) 的后缀链接为 \(0\) 。

  • 否则,存在一个 \(p\) 到 \(q\) 的转移,如果这个转移连续,这表明这个集合仍然满足是一个等价类,因为 \(q\) 中的字符串一定是由 \(p\) 和 \(p\) 的父亲经过转移得到的。所以我们直接把 \(cur\) 的后缀链接连上来就可以。

  • 反之,\(p\) 一定有多个儿子,且某个儿子能转移到 \(q\),这使得 \(q\) 中的字符串某些 \(endpos\) 会发生变化,某些不会变化,我们要把它分裂成两个状态,一个是变化的,一个是不会变化的,变化的那些就是 \(p\) 和 \(p\) 的父亲转移得到的字符串,变化的原因是我们往整个字符串 \(s\) 的末尾加了一个字符。分裂之后维护后缀链接即可。

对操作次数为线性的证明

一下我们认为字符集的大小为常数。

我们考虑算法的各个部分,有三处时间复杂度不明显是线性的:

  • 第一处是遍历所有状态 \(last\) 的后缀链接,添加字符 \(c\) 的转移。
  • 第二处是当状态 \(q\) 被复制到一个新的状态 \(clone\) 时复制转移的过程。
  • 第三处修改指向 \(q\) 的转移,将它们重定向到 \(clone\) 。

因为 SAM 状态数是线性的,而每个节点最多只有常数个转移,所以转移数也是线性的,所以第一处和第二处是线性的。

我们接下来证明第三处也是线性的。

在每一次添加字符时我们不妨关注一下 \(shortest(link(last))\) ,在向 \(s\) 中添加字符之前,有 \(shortest(p)\le shortest(link(last))\) ,这是因为 \(link(last)\) 至多是 \(p\) ,我们由 \(q\) 拷贝得到了节点 \(clone\) ,并试图从 \(p\) 沿后缀链接上溯,将所有通往 \(q\) 的转移重定向为 \(clone\) ,这时 \(shortest(clone)\) 是严格变小的,加完字符后,我们有 \(last=cur\rightarrow link(last)=link(cur)=clone\) ,所以 \(shortest(link(last))\) 是在严格变小的,而且减小的幅度和改变转移的次数级别相同,故我们改变转移也是线性的。

可以在线构造后缀自动机的神奇网站

其他应用

对于一个字符串,把所有字符串加进 Trie 里面,然后进行压缩之后得到的树,是后缀树。后缀树上一条边代表的是一个字符串。对于一个串 \(s\),我们建出反串的后缀自动机得到的 fail 树就是后缀树。

引用

有一些明显的错误已经在本文改正。

标签:状态,SAM,后缀,endpos,字符串,自动机,转移
From: https://www.cnblogs.com/HeNuclearReactor/p/17552213.html

相关文章

  • 后缀数组
    构造后缀数组我们会得到两个数组\(sa_i\)表示排名为\(i\)的后缀是哪一个,\(rk_i\)表示后缀\(i\)的排名。这里只讲倍增做法。原理非常简单,如下图:实现需要一个对两位关键字进行排序的基数排序,设\(y_i\)表示按照第二维关键字排序,排名为\(i\)的是哪一个位置。设\(rk_i......
  • 「学习笔记」后缀数组
    感谢LB学长的博文!前置知识后缀是指从某个位置\(i\)开始到整个串末尾结束的一个特殊子串,也就是\(S[i\dots|S|-1]\)。计数排序-OIWiki(oi-wiki.org)基数排序-OIWiki(oi-wiki.org)变量后缀数组最主要的两个数组是sa和rk。sa表示将所有后缀排序后第\(i\)小......
  • 挑战程序竞赛系列(70):4.7后缀数组(2)
    挑战程序竞赛系列(70):4.7后缀数组(2)传送门:POJ1509:GlassBeads题意:ThedescriptionofthenecklaceisastringA=a1a2…amspecifyingsizesoftheparticularbeads,wherethelastcharacteramisconsideredtoprecedecharactera1incircularfashion.Thedisjoin......
  • OpenCV计算机视觉学习(14)——浅谈常见图像后缀(png, jpg, bmp)的区别(opencv读取语义分割m
    如果需要处理的原图及代码,请移步小编的GitHub地址传送门:请点击我如果点击有误:https://github.com/LeBron-Jian/ComputerVisionPractice 本来不想碎碎念,但是我已经在图像后缀上栽倒两次了。而且因为无意犯错,根本找不到问题。不论是在深度学习的语义分割中,还是在图......
  • 2023ACM暑假训练day 9 后缀自动机SAM
    目录DAY9后缀自动机SAM训练情况简介题DAY9后缀自动机SAM训练情况简介2023-07-0709:20:38星期五题题意:思路:......
  • 后缀自动机SAM
    目录后缀自动机例题相关资料后缀自动机例题相关资料......
  • 【后缀自动机】Codeforces Round #305 (Div. 1) E. Mike and Friends
    对所有的串加特殊字符隔开,单串建立后缀自动机。然后将每个的fa边反向建树,对树dfs得到dfs序,对dfs序建立线段树。询问离线,每个询问拆成1-(l-1)和1-r。。。按端点排序,然后每次加入线段树,查询k对应的节点的子树和。。。#include<iostream>#include<queue>#include<stack>#include......
  • .NET各种常见后缀名的含义(.csproj,.suo,.resx......)
    https://blog.csdn.net/prefercent/article/details/8471816整理了一些.NET项目中经常接触但是不明白什么意义的文件后缀名,希望能帮到大家。.cs类文件。源代码都写在这里,主要就看这里的代码。.Designer.cs设计文件,自动生成.resx资源文件,一些资源存放在这里.csprojC#项目文件......
  • 2023-03-17- 后缀自动机
    我直接忽略掉这个玩意的原理。或许我应该从自动机开始,更确切地说我应该从确定有限状态自动机(\(DFA\))开始。\(\mathbb{DFA}\)\(\mathtt{Definition}\)首先给出一些前置的定义。\(Q\),有限状态集合。\(\Sigma\),有限字符集。\(\delta\),转移函数,\(Q\timesE\rightarrow......
  • ac自动机
    目录ac自动机相关资料ac自动机相关资料b站视频-邋遢大哥233......