发现自己根本没有 SAM 基础,所以想补一篇学习笔记。

SAM

SAM 是一个可以接受字符串 \(s\) 的所有后缀的最小 \(DFA\)(确定性有限状态自动机)。不过他最大的用处和后缀数组一样,都是用来处理子串信息的。既然他是 \(DFA\),那他就是 \(DAG\),下文的 \(DAG\) 都代指这张图。同时这个 \(DAG\) 有且只有一个出发点 \(P\),满足没有点可以到它但是所有点都可以从它抵达。

endpos 等价类

对于一个字符串 \(S\) 的一个子串 \(s\),定义 \(endpos(s)\) 表示 \(s\) 在 \(S\) 中每个出现位置的右端点所形成的集合。

例如,\(S=ababbabaab,s=ab\) 时,\(endpos(s)=\{2,4,7,10\}\)。

我们容易发现在上面的字符串中,\(aa\) 和 \(baa\) 的 \(endpos\) 相等。同样的情况还有很多组。我们将所有 \(endpos\) 相等的子串归为一个等价类。我们定义 \(endpos(E)\) 代表等价类 \(E\)(是一个字符串集合)对应的 \(endpos\) 集合。

\(DAG\) 的每个点都会代表一个 \(endpos\) 等价类,且两两不同。

各种引理

下文中,\(|s_1|\ge|s_2|\)。

  1. “\(endpos(s_1)=endpos(s_2)\)” 是 “\(s_2\) 是 \(s_1\) 的后缀,且在 \(S\) 中每次都会以后缀形态出现。”的充要条件。
  2. 若 \(s_2\) 为 \(s_1\) 后缀,则 \(endpos(s_1)\subseteq endpos(s_2)\),否则 \(endpos(s_1)\cap endpos(s_2)=\emptyset\)。
  3. 一个 \(endpos\) 等价类中不会包含两个长度相同但本质不同的字符串。
  4. 一个 \(endpos\) 等价类中的字符串长度一定是连续出现的。

link 和后缀树

对于某个不是 \(P\) 的节点 \(v\),定义 \(s\) 为节点 \(v\) 代表的的字符串中最长的一个。记字符串 \(t\) 表示最长的不和 \(s\) 在同一等价类的 \(s\) 的后缀,我们认为 \(t\) 所在的等价类为 \(E'\),\(s\) 所在等价类为 \(E\)。我们定义 \(link(E)=E'\)。若我们从 \(E\) 向 \(E'\) 连一条边,可以证明我们将会得到一棵内向树,称之为后缀树。显然,若我们从 \(E\) 开始一直跳到 \(P\),我们就可以遍历 \(s\) 的所有后缀。

各种引理

  1. \(link(E)\) 中最长的字符串 \(s\) 是 \(E\) 中最短的字符串 \(t\) 长度为 \(|t|-1\) 的后缀。
  2. \(endpos(E)\subsetneq endpos(link(E))\)。
  3. 后缀树的形态是一棵树(我们刚才并没有证明他的形态)。

实现

现在我们大概能想到,SAM 是由 DAG 和后缀树组成的。两部分相互关联而又独立,造就了 \(SAM\) 的毒瘤。

现在,我们定义 DAG 中节点 \(v\) 所对应的等价类为 \(E_v\),\(E_v\) 中最长的字符串为 \(r(v)\),最短的为 \(l(v)\)。

那么我们就可以开始讲解流程了。

算法流程

最开始时,整个 SAM 只有一个点 \(P\)。我们规定 \(|r(P)|=0,link(P)=-1\)。现在我们添加一个字符 \(c\),流程如下:

  1. 令 \(last\) 为添加之前整个字符串所对应的节点。初始时 \(last=0\)。
  2. 创建一个新节点 \(cur\),显然 \(|r(cur)|=|r(last)|+1\)。
  3. 在后缀树上,从 \(last\) 开始遍历,如果当前节点 \(p\) 在 DAG 上没有标记为 \(c\) 的出边,我们就在 DAG 上创建一条有向边 \(p\to cur\),标记为 \(c\)。
  4. 如果当前节点 \(p\) 在 DAG 上有标记为 \(c\) 的出边,我们就停止遍历,并记通过 DAG 上标记为 \(c\) 的边到达的节点为 \(q\):
    1. 若 \(|r(p)|+1=|r(q)|\),令 \(link(cur)=q\)(显然 \(r(p)\) 是 \(r(last)\) 的后缀,\(|r(p)|+1=|r(q)|\) 根据引理3,相当于 \(r(q)\) 也是 \(r(cur)\) 的后缀,那么 \(q\) 中的所有字符串 \(endpos\) 仍然相等)。
    2. 否则再建立一个点 \(cpy\),继承 \(q\) 除了名字以外的所有信息(包括 \(link\) 和 DAG 上的所有出边),同时令 \(|r(cpy)|=|r(p)|+1,link(q)=link(cur)=cpy\),再从 \(p\) 开始遍历(实际上此时 \(q\) 中的所有字符串的 \(endpos\) 已经不等了,而且显然分为了两个部分。这一步相当于将 \(q\) 拆成 \(r(p)+c\) 的后缀 \(cpy\) 和其他字符串 \(q\) 两个部分):
      1. 如果当前节点 \(v\) 在 DAG 上有标记为 \(c\) 的出边 \(v\to q\),则将之删除,并改为 \(v\to cpy\)。
      2. 否则停止遍历。
  5. 如果遍历到 \(P\) 了,\(link(cur)=0\)(相当于之前不存在字符 \(c\))。
  6. 令 \(last=cur\),并且结束这次插入。

时空复杂度

首先需要声明,一般来说,DAG 的连边方式和 \(Trie\) 类似,所以假如我们设字符串中字符数量为 \(m\),则 SAM 空间复杂度为 \(O(nm)\)。若使用映射可以将空间复杂度降至 \(O(n)\),但时间复杂度也会相应上升,这要看你使用的是平衡树还是哈希表。

节点数

显然不超过 \(2n-1\)。能达到上限的字符串有 \(S=abb\cdots bb\)。

边数

整个 SAM 的边数可以证明不超过 \(3n-4\)。能达到上限的字符串有 \(S=abb\cdots bbc\)。

代码

//Luogu P3804
#include<bits/stdc++.h>
using namespace std;
const int N=2e6+5;
string s;long long ans;
namespace SAM{
int tot,link[N],len[N];
int lst,sz[N],tr[N][26];
vector<int>g[N];
void insert(int c){
int cur=++tot,p=lst;
len[cur]=len[lst]+1,sz[lst=cur]++;
while(!tr[p][c]&&~p)
tr[p][c]=cur,p=link[p];
if(p<0) return;
int q=tr[p][c];
if(len[q]==len[p]+1)
return link[cur]=q,void();
int cpy=++tot;link[cpy]=link[q];
for(int i=0;i<26;i++)
tr[cpy][i]=tr[q][i];
len[cpy]=len[p]+1;
link[q]=link[cur]=cpy;
while(tr[p][c]==q)
tr[p][c]=cpy,p=link[p];
}void build(){
for(int i=1;i<=tot;i++)
g[link[i]].push_back(i);
}void dfs(int x){
for(auto y:g[x]) dfs(y),sz[x]+=sz[y];
if(sz[x]>1) ans=max(ans,1ll*sz[x]*len[x]);
}
}int zh(char c){return c-'a';}
int main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>s,SAM::link[0]=-1;
for(int i=0;s[i];i++)
SAM::insert(zh(s[i]));
SAM::build(),SAM::dfs(0);
cout<<ans;
return 0;
}

exSAM

SAM 虽好,但仍有局限性:只能针对一个字符串建立。假如有多个字符串,固然可以在字符串间加入特殊字符的方式解决,但每个特殊字符也必须不同。这样就犯了字符数量过大的忌讳,导致时空复杂度增大。

我们能否像 ACAM 一样,通过 \(Trie\) 树建立 exSAM 呢?

修改定义

在 SAM 中,我们有三大定义:后缀、\(endpos\)、\(link\)。

对于后缀,设 \(Trie\) 树 \(T\) 上从 \(x\) 到 \(y\) 的路径为 \(T_{x\to y}\),当然,\(x\) 必须为 \(y\) 的祖先。那么 \(T\) 的后缀集合就可以表示为 \(\{T_{x\to y}|r_y=1\}\)。其中 \(r_x\) 表示 \(x\) 的度数。

对于 \(endpos\) 的新定义也顺水推舟,即 \(endpos(s)=\{y|T_{x\to y}=s\}\)。那 \(link\) 就可以不变了。

离线方法

由于 \(dfs\) 死法多多,所以我只说 \(bfs\)。

考虑在 \(Trie\) 上 \(bfs\)。对于非根节点 \(x\),若要将其插入,\(last\) 应该是其父亲在 exSAM 上的编号,所以要对于每一个 \(Trie\) 树上的节点记录其在 exSAM 中对应的编号,其他操作和普通 SAM 相同。可以证明是有正确性的(dfs 就没这么好了)。

设 \(Trie\) 树点数为 \(n\),\(Trie\) 树所代表的所有字符串长度总和为 \(k\),那么 \(bfs\) 的时间复杂度是 \(O(nm)\) 的,远优于 dfs 和在线的 \(O(km)\)。

在线方法

考虑每新加入一个字符串,就把 \(last\) 归一次 \(0\),再加入少许特判,就可以避免空节点问题。实际上,\(dfs\) 和在线的 \(insert\) 相同的。

SAM 学习笔记的更多相关文章

  1. SAM学习笔记

    SAM学习笔记 后缀自动机(模板)NSUBSTR(Caioj1471 || SPOJ 8222) [题意] 给出一个字符串S(S<=250000),令F(x)表示S的所有长度为x的子串中,出现次 ...

  2. 【文文殿下】后缀自动机(Suffix Automaton,SAM)学习笔记

    前言 后缀自动机是一个强大的数据结构,能够解决很多字符串相关的(String-related)问题. 例如:他可以查询一个字符串在另一个字符串中出现的所有子串,以及查询一个字符串中本质不同的字符串的个 ...

  3. 后缀自动机SAM学习笔记

    前言(2019.1.6) 已经是二周目了呢... 之前还是有一些东西没有理解到位 重新写一下吧 后缀自动机的一些基本概念 参考资料和例子 from hihocoder DZYO神仙翻译的神仙论文 简而 ...

  4. 后缀自动机(SAM) 学习笔记

    最近学了SAM已经SAM的比较简单的应用,SAM确实不好理解呀,记录一下. 这里提一下后缀自动机比较重要的性质: 1,SAM的点数和边数都是O(n)级别的,但是空间开两倍. 2,SAM每个结点代表一个 ...

  5. SAM学习笔记&AC自动机复习

    形势所迫,一个对字符串深恶痛绝的鸽子又来更新了. SAM 后缀自动机就是一个对于字符串所有后缀所建立起的自动机.一些优良的性质可以使其完成很多字符串的问题. 其核心主要在于每个节点的状态和$endpo ...

  6. <老友记>学习笔记

    这是六个人的故事,从不服输而又有强烈控制欲的monica,未经世事的千金大小姐rachel,正直又专情的ross,幽默风趣的chandle,古怪迷人的phoebe,花心天真的joey——六个好友之间的 ...

  7. Jade学习笔记

    初学nodejs,折腾过用handlebars做模板,后来隔了一段重新学习,用了jade,真心简洁……记录一些学习笔记,以备复习. jade是基于缩进的,所以tab与space不能混用: 属性的设置: ...

  8. 《C++ Primer Plus》学习笔记6

    <C++ Primer Plus>学习笔记6 第11章 使用类 <<<<<<<<<<<<<<<&l ...

  9. Java8学习笔记----Lambda表达式 (转)

    Java8学习笔记----Lambda表达式 天锦 2014-03-24 16:43:30 发表于:ATA之家       本文主要记录自己学习Java8的历程,方便大家一起探讨和自己的备忘.因为本人 ...

  10. JavaSE中Collection集合框架学习笔记(2)——拒绝重复内容的Set和支持队列操作的Queue

    前言:俗话说“金三银四铜五”,不知道我要在这段时间找工作会不会很艰难.不管了,工作三年之后就当给自己放个暑假. 面试当中Collection(集合)是基础重点.我在网上看了几篇讲Collection的 ...

随机推荐

  1. 【返回值】定义泛型JSON

    /** * 定义统一的Json结构 * 由于封装的Json数据的类型不确定,所以在定义统一的json结构时,我们需要用到泛型. * 统一的json结构中属性包括:数据.状态码.提示信息即可. * 构造 ...

  2. 销讯通-CRM系统的功能远远不止于用来打卡

    在信息化的过程中,CRM系统其实很多企业都在用,最开始的设想是很好的,大家用着之后发现它可能最终只会沦为一个上班打卡考核或者是最基础的一个签到工具了,没有发挥它应有的一个功能. 最基础的一个诉求 我们 ...

  3. Win10虚拟机安装Docker解决Docker Engine Stopped问题记录

    跟着网上的帖子开启WSL2安装DockerDesktop, 但是无法启动Docker,一直[Docker Engine stopped] 继续跟着网上的帖子解决问题,检查电脑各种配置都搞一通后还是无法 ...

  4. Gitbook在Docker中安装插件的方法

    别光在那百度,什么先book.json添加plugin,然后在install? 哥,你现在用的是docker哎,docker都启动不了,你如何gitbook install呢? 乖乖听我的,按我的方法 ...

  5. nginx-tengine-invalid IPv6 address in resolver-解析器中无效的IPv6地址

    问题描述:解析器中无效的IPv6地址 [root@dm ~]# nginx -t nginx: [emerg] invalid IPv6 address in resolver "[fe80 ...

  6. Specifications动态查询

    [前言说明] 针对CRUD种的查询,因为我们的查询总是具有各种各样的筛选条件 为了我们的程序能够更加适应筛选条件的变化,SpringDataJpa提供了Specifications这种解决方案 Spe ...

  7. docker.sock: connect: permission denied 解决

    问题描述xjun@DESKTOP-L2R4GKN:~$ docker run -it hello-worlddocker: Got permission denied while trying to ...

  8. pmml(模型标准化)

    PMML简介PMML全称预言模型标记模型(Predictive Model Markup Language),以XML 为载体呈现数据挖掘模型.PMML 允许您在不同的应用程序之间轻松共享预测分析模型 ...

  9. 视频监控推流助手/极低延迟/支持N路批量多线程推流/264和265推流/监控转网页

    一.前言说明 搞视频监控开发除了基本的拉流以外,还有个需求是推流,需要将拉到的流重新推流到流媒体服务器,让流媒体服务做转发和负载均衡,这样其他地方只需要问流媒体服务器要视频流即可.为什么拉了又重新推呢 ...

  10. Qt编写可视化大屏电子看板系统23-模块1产量汇总

    一.前言 大屏系统采用结构模块化的分层设计思路,一个表对应一个最小模块比如模具产量.零件产量,数据库采集的时候采集对应的表,拿到数据后按照对应的数据规则传给控件绘制,其中模具产量.零件产量两个模块采用 ...