首页 > 编程语言 >『算法小记』SAM

『算法小记』SAM

时间:2023-09-03 12:56:14浏览次数:36  
标签:子串 SAM tree 算法 endpos daduoli 节点 小记

引入

  daduoli最近对自己的名字很感兴趣,于是他开始研究自己的名字。知周所众,搞清楚一个字符串的最好方法就是把他的所有子串列出来(误),所以daduoli开始尝试列举他名字中所有的子串。

  列了好一会,daduoli发现子串太多了,于是尝试把它们拼在一起。拼了好一会儿,他拼出来一个奇怪的东西。此时shui_dream路过:“你咋拼了个SAM?”

  (以上故事情节纯属虚构,如有雷同,那就雷同)

算法原理

\(trie\)

  事情还要从daduoli拼的过程说起。

  daduoli打算先拿他的姓试试水(知周所众,daduoli姓daduo),他考虑将姓的所有的后缀放到一个 \(trie\) 树上,它看起来是酱紫的:

  

  发现这个 \(trie\) 有许多非常好的性质:

  1. 有一个源点,若干个终止点。边代表在目前的字符串后加上的字母。从源点到任意一个节点的任意路径可以形成一个字符串
  2. 从源点到任意节点的任意路径形成的字符串均为原串子串。从源点到任意节点的任意路径不能形成的字符串均不为原串子串(简单来说,这个图可以表示,且仅可以表示出原串的所有子串)
  3. 从源点到任意终止节点的任意路径形成的字符串均为原串后缀
  4. 从源点出发的任意两条不同路径形成的字符串不相同

  虽然这个 \(trie\) 能完美满足daduoli的要求,但daduoli并不满意,应为这棵树的节点数实在是太多了,最坏情况会是 \(O(n^2)\) 的,如果daduoli名字在长个十倍的话,建出来的 \(trie\) 对于密恐的daduoli来说就太恐怖了。

  突然,daduoli发现了这个 \(trie\) 是可以压缩的,他尝试压缩掉了一个点:

  

  daduoli发现即使trie变成了一个DAG,他仍然能在上面找到所有的子串,于是daduoli开始思考如何把原trie压缩成一个点数尽可能少的DAG。

  就在daduoli苦思冥想时,突然受到仙人托梦,醒了之后立刻就理解了一切()

  接下来让我们看看daduoli理解了什么:

  (daduoli觉得自己的名字真不好拼,于是下两个小节的所有图片将描述字符串"\(aababa\)"。)

\(endpos\)

定义

  对于原串的一个子串 \(p\) ,\(endpos(p)\) 的值为\(p\)在原串中出现的所有位置的右端点集合。举个栗子,对于串"daduo"而言,\(endpos(\)"\(d\)"\()=\{1,3\}\) 。

结论1

\[\color{Red}结论1:如果两个子串的 endpos 相同,则其中子串一个必然为另一个的后缀 \]

  设两个子串中较短的串为 \(t\) , 较长的串为 \(s\) ,用反证法假设两个子串的后\(|t|\)个字符中至少有一个不相同,但是因为两串的 \(endpos\) 相同,所以这个假设恒为假。QED.

结论2

\[\color{Red}结论2:对于任意两个子串 t 和 p(|t|<|p|) ,[endpos(p) \in endpos(t)] xor [endpos(p) \cap endpos(t) = \varnothing] = 1 \]

  结论1的逆命题,分类讨论 \(t\) 为或不为 \(p\) 的后缀即可。

定义:\(endpos\) 等价类

  \(endpos\) 相同的两个子串归为一个 \(endpos\) 等价类。

结论3

\[\color{Red}结论3:一个 endpos 等价类中的串长度连续 \]

  由结论2可知,长度是连续的。并且任意的两个 \(endpos\) 等价类不会包含同一个子串。

  后面的推论会比较重要

结论4

\[\color{Red}结论4:endpos 等价类个数的级别为 O(n) \]

  对于一个 \(endpos\) 等价类(后文简称类),根据结论3,存在一个长度最长的串 \(p\) ,在 \(p\) 前添加一个字符使得字符串仍为原串的子串,得到的串 \(t\) 必然不属于 \(p\) 所在的类,并且由结论2我们可以知道该串的 \(endpos(t)\) 必然是 \(endpos(p)\) 的子集。

  发现在 \(p\) 前加的字符如果不同,设形成的子串分别为 \(t_1\) 与 \(t_2\) ,那么由结论2可知\(endpos(t_1) \cap endpos(t_2) =\varnothing\)。

  由上面的两个小结论,我们就可以把“在 \(p\) 前添加一个字符使得字符串仍为原串的子串”这个操作看做对 \(endpos(p)\) 进行分割,割成若干 \(endpos(t_x)\) 。这个操作像极了daduoli天天钻研的data structure:线段树。不难发现线段树这样二分的分割所产生的子集个数是最多的,但节点数也才堪堪 \(2n-1\) ,所以类个数数量级 为 \(O(n)\) 。

后缀自动机

定义:\(parent \enspace tree\)

  daduoli发现可以把等价类之间的分割表示成一棵树:

  

  这样形成类与类之间父子关系的一棵树,就叫 \(parent \enspace tree\) 。

结论5

\[\color{Red}结论5:一个类中,最短的子串长度等于其父亲最长子串+1 \]

  其实证明早就在结论4第二个小结论的推导中便已给出。

  这个结论结合结论2,我们就只需在 \(parent \enspace tree\) 的节点保存该类的最长子串即可,其他子串都可以通过一步步去掉前缀直到父亲的最长子串长度\(+1\) ,这样的 \(parent \enspace tree\) 长这样:

  

定义:后缀自动机

  后文简称后缀自动机为 \(SAM\)

  在daduoli坚持不懈地拼字符串后,\(SAM\) 终于是给他拼出来了。

  \(SAM\) 的节点是 \(parent \enspace tree\) 中的节点,\(parent \enspace tree\) 的根节点(即空串所在的类)是 \(SAM\) 的源点,而终止节点则是整个原串所在类的节点以及该节点在 \(parent \enspace tree\) 上的所有祖先。

  虽然daduoli把 \(SAM\) 的雏形给搞出来了,但仙人并没有告诉他怎么连边,也没有告诉他到底怎么用。正当daduoli感到疑惑时,shui_dream过来了,这才发生了开头的一幕。

  (接下来是shui_dream的主场!)

  shui_dream大手一挥,往daduoli的 \(SAM\) 上连上好几条边,daduoli定睛一看,恍然大悟:

  

  不难看出,沿着原 \(parent \enspace tree\) 的边走,就是在串的前面加字符,而沿着非 \(parent \enspace tree\) 的边走,则相当于在边的后面加字符。

  发现 \(SAM\) 完美满足本节开头所提到 \(trie\) 的性质。前三个性质显然满足,对于最后一个,因为类是分割形成的,所以不同的类中不会包含同一个串,因此到任意两个不同节点所形成的字符串均不重复。接下来说明 \(SAM\) 存在。

  \(SAM\) 的存在性取决于边的正确性,因此下面论证边的正确性。显然沿着 \(parent \enspace tree\) 的边走是没有任何问题的,问题就是非 \(parent \enspace tree\) 的边是否正确。对于一个类中的所有子串,在其后添加一个字符,他们一定在另一个相同的类中,所以非 \(parent \enspace tree\) 的边也是正确的。从而 \(SAM\) 存在。

  更严谨的证明将会在后文提到,这里只是进行并不那么严谨的解释。

结论6

\[\color{Red}结论6:SAM$ 的边数为 $O(n) \]

  又臭又长,直到有这个东西就行了,先不整了

  反正写代码不考证明

构造

基本形态

  daduoli非常想要学习 \(SAM\) ,于是死皮赖脸地让shui_dream教他构造(说是要帮shui_dream代打舟游)。没有办法,shui_dream只能再次大手一挥,画了一张描述 \(SAM\) 形态的图片:

  

  daduoli根本看不懂,于是shui_dream解释道:下面整齐的一行表示各个前缀所属的节点。一个前缀显然是所属类中长度最长的,因为它前面不能再加字符了。相邻的两个前缀显然一定会连上非树边,但是不相邻的点之间也会有边,所以这些节点放在最下面,方便观察。

  上方零散的点是不包含任意前缀的类,一些边连下来,一些边连上去,内部还有一些边。这就是 \(SAM\) 的基本形态(结构)。

Code

  我相信一定有人会直接跳到这看我丑陋的马蜂的(说的就是你Cust10)

  “\(SAM\) 的构造是在线的,相当于把原串拆成一个个字符,通过不断把字符塞进 \(SAM\) 进行构造。”shui_dream这样说到。

  突然,天上掉下来一个大大的代码框和链接,想必是daduoli认真学习的样子感动了仙人吧()link

点击查看代码

  这种题就没必要放评测记录了吧……

  即使有仙人所写的精美注释,daduoli看见这玩意一脸懵逼,(我也是),于是旁边一脸轻松的shui_dream开始给daduoli一段段地讲解。

int p = pre, np = pre = ++cnt;
a[np].len = a[p].len+1;

  在这段程序段中,\(pre\) 表示未加入字符 \(x\) 之前,整个串所属的节点编号,\(cnt\) 则表示整个 \(SAM\) 的节点总数。

  加入了字符 \(x\) 后,显然整个串的长度就变长了,因此需要新建一个节点 \(np\) ,并且 \(len(np) = len(p)+1\) 。

  注意到在加入字符 \(x\) 之后 \(endpos\) 发生变化的串只有可能是新串的后缀,所以接下来就需要遍历每一个旧串的后缀,看看加上字符 \(x\) 之后 \(endpos\) 有没有变化。

for (; p && !a[p].ch[x]; p = a[p].fa) a[p].ch[x] = np;

  这个语句其实就是上述操作,去遍历每一个旧串后缀,然后尝试加上字符 \(x\) 。 p = a[p].fa 这个在 \(parent \enspace tree\) 跳父亲的操作,就是在不断寻找更短的前缀。而如果这个节点有 \(x\) 的出边,就说明之前有这个子串,不能把它丢到新的节点,这就是!a[p].ch[x]的意义。至于p只是为了防止自由奔放地跳出根节点。

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

  如果说最后 \(!p\) ,就说明新加的字符没有产生任何一个旧串中已有的子串。长度为 \(0\) 的串 \(+x\) 都不是旧串中的子串,说明 \(x\) 是全新的字符,它的父亲就应该是空串。

int q = a[p].ch[x];
if (a[np].len == a[p].len+1) a[np].fa = q;

  \(x\)不是新字符,那么某个旧串后缀就会有一个 \(x\) 的出边。在第二节后缀自动机定义处有提到这样的边完全覆盖了整个类 \(np\) ,因此如果它们的最长串之间的长度差为 \(1\) ,说明它们之间只差了一个字符 \(x\) ,直接将 \(q\) 作为 \(np\) 的父亲就好。

  最后的最后,我们的daduoli把他的名字塞进了 \(SAM\) 里,可喜可贺,可喜可贺。(daduoli:我只是想划水)

  感谢daduoli与shui_dream的友情出演()

应用

1. 判断子串

2. 不同子串个数

3. 所有子串中字典序第 \(i\) 大的是哪个

4. 判断最长公共子串

  最后的最后,学习一下高德纳:感谢你阅读本文,希望本文对你有用。

标签:子串,SAM,tree,算法,endpos,daduoli,节点,小记
From: https://www.cnblogs.com/BlackCrow/p/17673521.html

相关文章

  • Java使用有限状态机算法实现判断字符串是否合法
    题目描述请根据给出的正则表达式来验证邮箱格式是否合法,如果用户输入的格式合法则输出「邮箱格式合法」,否则输出「邮箱格式不合法」。正确格式对应的正则表达式"[a-zA-Z0-9]+@[a-zA-Z0-9]+\.[a-zA-Z0-9]+";输入:[email protected]输出:邮箱格式合法分析最容易想到的是正则表达......
  • 11种常用滤波算法程序
    来源:嵌入式情报局一、限幅滤波法(程序判断滤波法)1/*2A、名称:限幅滤波法(又称程序判断滤波法)3B、方法:4根据经验判断,确定两次采样允许的最大偏差值(设为A),5每次检测到新值时判断:6如果本次值与上次值之差<=A,则本次值有效,7如果本次值与上次值之差>......
  • Java:使用javax.crypto.Cipher的AES算法实现数据加密解密
    AES算法加密Stringalgorithm="AES/ECB/PKCS5Padding";//定义加密算法Stringkey="1234567890123456";//这是待加密的信息Stringmessage="HelloWorld.";//这是待加密的信息Ciphercipher=Cipher.getInstance(algorithm);cipher.init(Cipher.ENCRYPT......
  • 代码随想录算法训练营第二十五天| 216.组合总和III 17.电话号码的字母组合
     216.组合总和III    卡哥建议:如果把 组合问题理解了,本题就容易一些了。    题目链接/文章讲解:https://programmercarl.com/0216.%E7%BB%84%E5%90%88%E6%80%BB%E5%92%8CIII.html   视频讲解:https://www.bilibili.com/video/BV1wg411873x  做题思路:......
  • 【DBN分类】基于北方苍鹰算法优化深度置信网络NGO-DBN实现轴承故障分类matlab代码
    ✅作者简介:热爱科研的Matlab仿真开发者,修心和技术同步精进,matlab项目合作可私信。......
  • IIR滤波器算法
    IIR(InfiniteImpulseResponse)滤波器是一类递归型数字滤波器,其输出信号不仅与当前的输入信号有关,还与之前的输入和输出信号有关。因此,IIR滤波器的阶数相对较低,可以实现更为复杂的频率响应。IIR滤波器的数学模型描述如下:其中,x(n)和y(n)分别表示输入信号和输出信号,bk和ak分别为前向系......
  • 如何有效的刷算法题
    1.难度循序渐进2.按算法分类选题3.解题三部曲a.看懂题目b.分析、推导题目c.将思路转换成代码 https://github.com/MisterBooo/LeetCodeAnimationhttps://github.com/azl397985856/leetcodehttps://github.com/jwasham/coding-interview-university/blob/ma......
  • Lnton羚通智能分析算法基于智能算法的石油化领域生产作业流程合规检测系统
    石油化工领域的生产作业流程合规检测对于保障工厂安全运行至关重要。本文介绍了一种基于智能算法的生产作业流程合规检测方法,在传感器数据分析和模式识别方面应用了机器学习技术,提高了检测效果和准确性。通过该方法,可以及时发现和纠正不合规操作,最大限度地降低事故风险。石油化工领......
  • Lnton 羚通智能分析算法工服智能监测预警算法
    工服智能监测预警系统通过yolov8网络模型算法,工服智能监测预警算法对现场人员未按要求穿戴工服工装则输出报警信息,通知后台人员及时处理。Yolo算法采用一个单独的CNN模型实现end-to-end的目标检测,核心思想就是利用整张图作为网络的输入,直接在输出层回归boundingbox(边界框)......
  • Lnton羚通智能分析算法检测人群异常聚集检测告警算法的流程代码
    Lnton羚通视频智能分析算法中人群异常聚集检测报警系统是基于yolov8图像识别和数据分析技术,人群异常聚集检测告警算法通过在关键区域布设监控摄像头,实时监测人员的密集程度和行为动态,分析和判断人群密集程度是否超过预设阈值,一旦发现异常聚集,将自动发出信号,并提示相关人员采取相应......