【Coel.学习笔记】后缀自动机

news/2024/5/9 11:08:30/文章来源:https://www.cnblogs.com/Coel-Flannette/p/16614448.html

来了!NOI 算法中最抽象的字符串算法——后缀自动机!
当然咱只是一个普通的小 OIer,不会搞那么多杂七杂八的ww

引入

后缀自动机(\(\text{Suffix Automaton}\),简称 \(\text{SAM}\))是一种确定性有限状态自动机(\(\text{Determined Finite Automaton}\),简称 \(\text{DFA}\)),通过把一个字符串的所有子串存储到一张有向无环图上,并借助图上的状态转移在线性时间内实现字符串的各种操作。

各种定义与性质

SAM 有点过于枯燥(emm),所以直接讲各种定义性质了,先不看例题~
取一张网上的图片作为例子。

性质一:子串对应性

在 SAM 上,所有从起点开始的路径能够与原字符串的子串一一对应,并且 SAM 的边数为 \(O(n)\) 级别。

性质二:节点包含性

每个节点可以对应若干个子串。每个子串都能够与一条从起点到该节点的路径,并且这些子串是其中最长串的连续后缀。
例如对于第四个节点,可以对应的子串有 \(\text{aabb,abb,bb}\),其中 \(\text{aabb}\) 的后缀是 \(\text{abb,bb}\)

性质三:不同的边

第一种边为图例上的蓝色边。类似字典树,可以接在每一个节点的后面,相当于给这个节点对应的所有子串加上一个字符。
还是用第四个节点举例,它的子串 \(\text{aabb,abb,bb}\) 到达节点六就变成了 \(\text{aabba,abba,bba}\)

第二种边为图例上的绿色边,称为 link 边或后缀链接。接在每个节点后时,相当于给最短字串去掉一个字符。
第四个节点的最短字串为 \(\text{bb}\),通过后缀链接到达节点五时变成了 \(\text{b}\)
这类边能够形成一棵树

构造思路

SAM 的构造需要一个叫做 \(\operatorname{endpos}\) 的东西。对于一个子串 \(s\)\(\operatorname{endpos}(s)\) 表示这个子串在字符串中出现时,所有结束位置的集合。

例如对于字符串 \(\text{aabbabd}\),有 \(\operatorname{endpos}(“ab”)=\{3,6\}\)

在某些情况下两个子串的 \(\operatorname{endpos}\) 可能相等,例如 \(\operatorname{endpos}(“aabb”)=\operatorname{endpos}(“abb”)=\{4\}\)。这种情况下,这些子串被称为 等价类。SAM 上的所有状态都能和每一个不同的等价类一一对应。

\(\operatorname{endpos}\) 的性质有:

  1. 对于两个非空子串 \(s_1\)\(s_2\)(设 \(|s_1|\leq |s_2|\)),若 \(\operatorname{endpos}(s_2)\sube\operatorname{endpos}(s_1)\),则字符串 \(s_1\)\(s\) 中仅以 \(s_2\) 的后缀形式存在;反之,若两个子串的 \(\operatorname{endpos}\) 完全无交,则 \(s_1\) 不为 \(s_2\) 的后缀。
    由上述性质可以得到一个推论:两个非空子串的 \(\operatorname{endpos}\) 只可能为包含关系或者完全无交。
  2. \(\operatorname{endpos}(s_1)=\operatorname{endpos}(s_2)\),则短字符串为长字符串的后缀。
  3. 对于一个 \(\operatorname{endpos}\) 等价类,若对应的最长子串的后缀 \(s\),满足其长度在最长串与最短串之间,那么 \(s\) 为在等价类对应的集合之中。
    换而言之,对于同一等价类的任一两子串,较短者为较长者的后缀,且该等价类中的子串长度恰好覆盖整个区间。

利用 \(\operatorname{endpos}\),我们就可以建立一个满足前面提到条件的 SAM 了。

构造采用增量插入。初始时 SAM 为空,每次把每个字符串添加进去,然后进行维护。

例题讲解

还是从模板开始。

【模板】后缀自动机

洛谷传送门
给定一个字符串,求出所有出现次数不为 \(1\) 的子串中,出现次数与该子串长度乘积的最大值。

解析:先看看怎么求每个子串的出现次数,相当于求出每个子串的 \(|\operatorname{endpos}|\)。求解时,我们只需要利用后缀链接边。

举一个例子,对于字符串 \(\text{aabcd,babcd}\),它们不为后缀关系所以 \(\operatorname{endpos}\) 完全无交;但它们都有一个后缀 \(\text{abcd}\),所以它们又都是 \(\operatorname{endpos}(“abcd”)\) 的一个子集。这也就说明了,所有子节点的 \(\operatorname{endpos}\) 其实是对父节点的一个划分。那么,我们只需要找到对应的父节点集合,并求出所有子节点的 \(|\operatorname{endpos}|\) 之和即可,一个 dfs 就可以搞定。

怎么找集合?其实也很简单,照着定义一步步往下走就行了。

#include <cstring>
#include <iostream>#define Miolic 0using namespace std;const int maxn = 2e6 + 10;  // 节点数为串串长度两倍char s[maxn];
long long ans;class Suffix_Automaton {private:int tot = 1, lst = 1;                      // lst 为上一个状态int head[maxn], nxt[maxn], to[maxn], cnt;  //构造自动机后存图long long f[maxn];  // f[i] 表示第 i 个字串的出现次数struct node {int len, fa;int ch[26];  //当字符集较大时使用 map 或哈希表} node[maxn];void add(int u, int v) { nxt[cnt] = head[u], to[cnt] = v, head[u] = cnt++; }public:void extend(int c) {int p = lst, nxp = lst = ++tot;f[tot] = 1;node[nxp].len = node[p].len + 1;  // 新状态长度等于原状态加一while (p && !node[p].ch[c])       // 一步步向下走node[p].ch[c] = nxp, p = node[p].fa;  // p 向前跳的目的是找到最长串if (!p) return node[nxp].fa = 1, (void)Miolic;//若新串后缀处理结束则直接跳出int q = node[p].ch[c];if (node[q].len == node[p].len + 1)  // 满足条件,直接把父亲向前指node[nxp].fa = q;else {  // 不满足条件,复制一个新节点并维护int nxq = ++tot;node[nxq] = node[q], node[nxq].len = node[p].len + 1;node[q].fa = node[nxp].fa = nxq;while (p && node[p].ch[c] == q) node[p].ch[c] = nxq, p = node[p].fa;}}void build() {  //按照 SAM 建图跑 dfsmemset(head, -1, sizeof(head));for (int i = 2; i <= tot; i++) add(node[i].fa, i);}void dfs(int u) {for (int i = head[u]; ~i; i = nxt[i]) {int v = to[i];dfs(v);f[u] += f[v];}if (f[u] > 1) ans = max(ans, f[u] * node[u].len);}
} SAM;int main(void) {ios::sync_with_stdio(false);cin.tie(nullptr);cin >> (s + 1);for (int i = 1; s[i]; i++) SAM.extend(s[i] - 'a');SAM.build();SAM.dfs(1);cout << ans;return 0;
}

可以发现,SAM 的代码量其实很少(核心代码只有三十来行),写起来也很方便。
上面注释提到,当字符集较大时要考虑使用哈希表或 map,这是因为我们给每个后缀都建了一个 ch 数组,当字符串长度很大时内存开销也非常恐怖 (这题就达到了可怕的 \(2\times10^7\),已经是内存上限的一半了)。
当然如果用了 map 时间复杂度就要多一个 \(\log n\),用哈希表同样要大内存,见仁见智吧。


下面看几道例题。很多 SAM 的题都可以用其他字符串算法(比如 KMP,AC 自动机,后缀数组)解决,做题时不妨对比一下几种算法的优劣。

[JSOI2012]玄武密码

洛谷传送门
给定一个模式串和若干个匹配串,对每个匹配串找到一个最长前缀的长度,满足该前缀为模式串的子串。

解析:这道题在之前讲 AC 自动机时提到过,可以看看。但是用 SAM 做这题,就非常简单了。

对模式串建立一个 SAM,那么 SAM 上每一个路径都可以对应一个子串。对每一个模式串,直接顺着 SAM 一直走,直到匹配失败时就是最长前缀。

呐,很简单吧?

//板子内容全部略过
inline int get(char c) {if (c == 'E') return 0;if (c == 'S') return 1;if (c == 'W') return 2;return 3;
}class Suffix_Automaton {private://...public://...int solve(char s[]) {int p = 1, res = 0;for (int i = 0; s[i]; i++) {int c = get(s[i]);if (node[p].ch[c])p = node[p].ch[c], res++;elsebreak;}return res;}
} SAM;int main(void) {ios::sync_with_stdio(false);cin.tie(nullptr);cin >> n >> m;cin >> s;for (int i = 0; s[i]; i++) SAM.extend(get(s[i]));while (m--) {cin >> s;cout << SAM.solve(s) << '\n';}return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_2522.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Python 实现雪花算法

Python 实现雪花算法 雪花算法:雪花算法是一种分布式全局唯一ID,一般不需要过多的深入了解,一般个人项目用不到分布式之类的大型架构,另一方面,则是因为,就算用到市面上很多 ID 生成器帮我们完成了这项工作。 介绍:Twitter 于 2010 年开源了内部团队在用的一款全局唯一 …

Python小游戏——外星人入侵(保姆级教程)第一章 06让飞船移动

Python小游戏——外星人入侵(保姆级教程) 第一章:武装飞船 06:让飞船移动 下面来让玩家能够左右移动飞船。我们将编写代码,在用户按左或右箭头键时做出响应。我们将首先专注于向右移动,再使用同样的原理来控制向左移动。通过这样做,你将学会如何控制屏幕图像的移动。系列…

web安全 - xss攻击与防御

xss(Cross-Site Scripting), 跨站脚本攻击。攻击者通过在目标网站上注入恶意脚本,使之在用户的浏览器上运行。 利用恶意脚本攻击者可以获取用户的敏感信息,Cookie, SessionID等,进而危害数据安全。 1. xss 类型 1. 存储型xss: 恶意脚本来源于数据库 由于恶意代码存储在服务器…

[NOIP2001 提高组] 一元三次方程求解

题目链接:https://www.luogu.com.cn/problem/P1024 试题分析: 三个答案都在[-100,100]范围内,两个根的差的绝对值>=1,保证了每一个大小为1的区间里至多有1个解,也就是说当区间的两个端点的函数值异号时区间内一定有一个解,同号时一定没有解。那么我们可以枚举互相不重叠…

oracle时间对比报错:ORA-01861 文字与格式字符串不匹配

前段时间写一个简单的需求时碰到的,在使用传入的时间参数与oracle数据库里存的时间进行比较时报错,具体错误如下:在Oracle中,需要使用to_date 格式化时间,再进行对比SELECT * FROM XXXXXXX t WHERE t.DATE BETWEEN to_date(2021-07-08 00:00:00,yyyy-mm-dd hh24:mi:ss…

大家都能看得懂的源码 - 封装一个管理 url 状态的 hook

本文是深入浅出 ahooks 源码系列文章的第十一篇,该系列已整理成文档-地址。觉得还不错,给个 star 支持一下哈,Thanks。 本文来讲下 ahooks 中的 useUrlState。通过 url query 来管理 state 的 Hook。useUrlState 的特殊 在之前的架构篇中我们就提到,ahooks 这个项目是一个 …

Element Ui使用技巧——Form表单的校验规则rules详细说明;element的 form 表单rules详细用法

介绍 Form 组件提供了表单验证的功能,只需要通过 rules 属性传入约定的验证规则,并将 Form-Item的 prop 属性设置为需校验的字段名即可。校验规则参见 async-validator文档中提及的用法有2种: 官方form 表单文档 https://element.eleme.io/#/zh-CN/component/form 1.对整个…

ssh连接windows10拒绝连接-SSH入站-windows开启SSH

第一步:ssh使用的22端口,首先确认windows10的22端口是否开启。--开启步骤 1.控制面板-->Windws Defender 防火墙-->高级设置-->入站规则-->新建规则2.选择端口-->下一步3.选择TCP-->输入开放的端口-->下一步4.允许连接-->下一步5.勾选 域、专用、公用…

测试右移-后台服务监控告警实践

前言 前段时间,公司上线了“大屏”项目,用于对接展示一些业务平台的数据。但是在上线后使用过程中,产品或业务经常反馈前台页面没有数据。出现这种情况后,开发人员会去排查问题,解决后再通知产品或业务人员解决修复情况。虽然研发每次都能在较短的时间内响应并解决问题,但…

windows许可证即将过期?快来小编告诉你解决方法

https://baijiahao.baidu.com/s?id=1598094917004791962&wfr=spider&for=pc很多使用win10系统的用户都有遇到过电脑提示你的windows许可证即将过期的问题,遇到许可证即将过期要怎么办呢?小编这里给大家介绍一下这个问题的解决方法。Windows系统都需要激活后才能使用…

TypeScript 数组中查找最小、最大n个元素

TypeScript 数组中查找最小、最大n个元素var typeArr:number[]=[1,10,50,6,80,9,100];//最小元素private minArr(arr:number[]){let minArray:number[]=[];//3 就是返回多少个for (let i = 0; i < 3; i++) {let min = arr[0]; for (let j = 0; j < arr.length…

多列转两列(Power Query)

问题:多列转一列,如下图,左表转成右表let源 = Excel.CurrentWorkbook(){[Name="表2"]}[Content],拆分列 = Table.ToColumns(源),列分组 = List.Split(拆分列, 2),列合并 = List.Transform(列分组, each Table.FromColumns(_)),升表头 = List.Transform(列合并, ea…

Android开发

1.知识点解析 1.1 dimen1.尺寸资源;2.在工程的res\layout\目录下创建一个test_dimen.xml布局文件。3.在该布局文件中添加一个TextView和一个Button。4.TextView的宽和高引用尺寸资源来设置,android:width="@dimen/text_width"5.dimen定义:<resources><di…

项目管理 WBS 分解法 All In One

项目管理 WBS 分解法 All In One Work Breakdown Structure 工作分解结构项目管理 WBS 分解法 All In OnePMPWork Breakdown Structure / 工作分解结构WBS工作分解结构跟因数分解是一个原理,就是把一个项目,按一定的原则分解,项目分解成任务,任务再分解成一项项工作,再把一…

如何在电脑桌面上显示待办任务清单?

如果你每天的待办工作任务都是很多的,那么你应该如何保证自己能够把每条待办任务管理的井井有条,并且按时完成每项工作任务呢?相信这是很多职场人士都面临的一个难题,每天的工作时间都是固定的,但工作任务却是又多又繁杂,所以就需要大家通过管理待办任务来提高办公效率。…

Stream流-流式思想概述和获取流

流式思想概述 整体来看,流式思想类似于工厂车间的“生产流水线”。 当需要对多个元素进行操作(特别是多步操作)的时候,考虑到性能及便利性,我们应该首先拼好一个“模型”步骤 方案,然后再按照方案去执行他 这张图中展示了过滤、映射、跳过、计数等多步操作,这是一种集合…

仓库的种类和彼此关系与Maven标准目录结构

仓库的种类和彼此关系 仓库分为三类:本地仓库,远程仓库,中央仓库 三类仓库直间的关系:在默认情况下启动一个Maven工程会从本地仓库找jar包,如果本地没有在连网状态下会从中央仓库下载jar包在公司中启动一个Maven工程会从本地仓库找jar包,本地没有会去远程仓库下jar包,如…

Fecify 自建私有化saas跨境商城系统

作为跨境的运营者,有多站需求的用户,可以通过wp,magento,fecmall等搭建多个跨境独立站,需要每个独立站单独安装,安装模板插件,配置等等,后续的管理维护比较繁琐,大多数的开源性能低下,插件安装冲突,模板调整问题等等,对于没有技术的个人和小公司,掌控难度高,很多…

# 【博学谷学习记录】超强总结,用心分享 | RabbitMQ消息的可靠性

RabbitMQ消息的可靠性消息队列在使用过程中,如何确保RabbitMQ消息的可靠性,如何确保发送的消息至少被消费一次?1.生产者消息确认 RabbitMQ提供了publisher confirm机制来避免消息发送到MQ过程中丢失。这种机制必须给每个消息指定一个唯一ID。消息发送到MQ以后,会返回一个结…

Learn Dijkstra For The Last Time

博客链接:https://www.codein.icu/learn-dijkstra/ Introduction Dijkstra 算法是用于求解非负权图单源最短路的经典算法。 市面上的大部分教程都仅仅停留在「如何实现 Dijkstra 算法」的层面。从应用角度,这当然无可厚非。但理解算法本身,也不失为一件乐事。 问自己这样几个…