当前位置: 首页 > news >正文

【学习笔记】后缀自动机(SAM)

前言

之前对后缀自动机的理解太浅薄了,故打算重新写一篇。

后缀自动机是什么

后缀自动机是一个字符串的所有后缀建起来的自动机。它把所有子串(后缀的前缀)用 O ( n ) O(n) O(n) 的空间装了起来。后缀自动机的边会构成一个 D A G DAG DAG

后缀自动机的一些定义

结束位置 endpos

对于一个子串 t t t,它在原串 s s s 里面的结束位置集合记为 e n d p o s t endpos_t endpost
比如 s = a b c b c s = abcbc s=abcbc t = b c t = bc t=bc,那么 e n d p o s t = { 2 , 4 } endpos_t = \{2,4\} endpost={2,4}(假设字符串从0开始编号)

等价类 u

所有 e n d p o s endpos endpos 相同子串是一个等价类,在后缀自动机上表现为一个点 u u u

后缀链接 link

其实就是我们常说的 f a i l fail fail
假设当前点为 u u u,设 w w w e n d p o s u endpos_u endposu 中最长的字符串,那么 l i n k u link_u linku 就会连向相对于 w w w 的最长后缀的另一个 e n d p o s endpos endpos 上,一定会满足:
m i n l e n u − 1 = l e n v minlen_u-1=len_v minlenu1=lenv

转移 nxt

t r i e trie trie 的转移,只不过所有的转移会构成一个 D A G DAG DAG

后缀自动机的一些性质

性质1

字符串 s s s 的两个非空子串 u u u w w w(假设 ∣ u ∣ ≤ ∣ w ∣ \left|u\right|\le \left|w\right| uw)的 endpos ⁡ \operatorname{endpos} endpos 相同,当且仅当字符串 u u u s s s 中的每次出现,都是以 w w w 后缀的形式存在的。

性质2

考虑两个非空子串 u u u w w w(假设 ∣ u ∣ ≤ ∣ w ∣ \left|u\right|\le \left|w\right| uw)。那么要么 endpos ⁡ ( u ) ∩ endpos ⁡ ( w ) = ∅ \operatorname{endpos}(u)\cap \operatorname{endpos}(w)=\varnothing endpos(u)endpos(w)=,要么 endpos ⁡ ( w ) ⊆ endpos ⁡ ( u ) \operatorname{endpos}(w)\subseteq \operatorname{endpos}(u) endpos(w)endpos(u),取决于 u u u 是否为 w w w 的一个后缀:

{ endpos ⁡ ( w ) ⊆ endpos ⁡ ( u ) if  u is a suffix of  w endpos ⁡ ( w ) ∩ endpos ⁡ ( u ) = ∅ otherwise \begin{cases} \operatorname{endpos}(w) \subseteq \operatorname{endpos}(u) & \text{if } u \text{ is a suffix of } w \\ \operatorname{endpos}(w) \cap \operatorname{endpos}(u) = \varnothing & \text{otherwise} \end{cases} {endpos(w)endpos(u)endpos(w)endpos(u)=if u is a suffix of wotherwise

性质3

考虑一个 endpos ⁡ \operatorname{endpos} endpos 等价类,将类中的所有子串按长度非递增的顺序排序。每个子串都不会比它前一个子串长,与此同时每个子串也是它前一个子串的后缀。换句话说,对于同一等价类的任一两子串,较短者为较长者的后缀,且该等价类中的子串长度恰好覆盖整个区间 [ m i n l e n , l e n ] [minlen,len] [minlen,len]

性质4

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

性质5

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

性质6

对于 t 0 t_0 t0 以外的状态 v v v,可用后缀链接 link ⁡ ( v ) \operatorname{link}(v) link(v) 表达 minlen ⁡ ( v ) \operatorname{minlen}(v) minlen(v)
minlen ⁡ ( v ) = len ⁡ ( link ⁡ ( v ) ) + 1. \operatorname{minlen}(v)=\operatorname{len}(\operatorname{link}(v))+1. minlen(v)=len(link(v))+1.

后缀自动机的构建

S A M SAM SAM 的构建是动态的,是在线的,每次插入一个字母。
这里先声明几个定义:

  • c u r cur cur:当前节点
  • l a s t last last:上一个字母代表的节点
  • f a u fa_u fau:就是 l i n k u link_u linku
  • n x t u [ c ] nxt_u[c] nxtu[c]:后缀自动机上的边,同 t r i e trie trie
  • l e n u len_u lenu e n d p o s u endpos_u endposu 中最长的字符串的长度。

下面是构建方法(假设当前字符为 c) :

  1. 首先我们初始化 l a s t = 1 last = 1 last=1,即 t 0 = 1 t_0=1 t0=1,代表初始节点(空字符串)
  2. 创建一个新状态 c u r cur cur,并令 l e n c u r = l e n l a s t + 1 len_{cur}=len_{last}+1 lencur=lenlast+1 l e n len len 的定义),此时 f a c u r fa_{cur} facur 无法确定,我们需要在后续的步骤确定它。
  3. p = l a s t p = last p=last,如果 n x t p [ c ] nxt_p[c] nxtp[c] 还不存在,那么就令 n x t p [ c ] = c u r nxt_p[c] = cur nxtp[c]=cur,并让 p p p沿着 f a fa fa 往上跳,即 p = f a p p=fa_p p=fap,重复这个过程,直到 n x t p [ c ] nxt_p[c] nxtp[c] 存在或者发现这样的 p p p 不存在。
  4. 如果找不到合法的 p p p p p p 就会跳到虚拟节点 0 0 0,我们令 f a c u r = 1 fa_{cur}=1 facur=1 并退出。
  5. 如果找到了合法的 p p p,我们记 q = n x t p [ c ] q = nxt_p[c] q=nxtp[c]
  6. 如果 l e n p + 1 = l e n q len_p+1=len_q lenp+1=lenq,那么可以直接令 f a c u r = q fa_{cur}=q facur=q l i n k link link的性质)
  7. 否则,就将当前状态 q q q 克隆一份,记为 r r r,并将 l e n r len_r lenr 赋值为 l e n p + 1 len_p+1 lenp+1。复制之后,我们令 f a c u r = r fa_{cur}=r facur=r。最后,从状态 p p p 开始沿着 f a fa fa 往上走,如果遇见 n x t p [ c ] = q nxt_p[c]=q nxtp[c]=q 就把它改为 r r r,否则就停下来,过程结束。
struct SAM
{int fa,len;vector<int> nxt;SAM(){fa=0; len=0;nxt.assign(27,0);}
};
vector<SAM> sam;
vector<int> dp;//这里的dp求的是 endpos 集合的 size
int last;
void insert(char c)
{int ch=c-'a';sam.push_back(SAM());int cur=sam.size()-1,p;sam[cur].len=sam[last].len+1;dp.push_back(1);for(p=last; p&&sam[p].nxt[ch]==0; p=sam[p].fa){sam[p].nxt[ch]=cur;}int q=sam[p].nxt[ch];if(q==0){sam[cur].fa=1;}else if(sam[q].len==sam[p].len+1){sam[cur].fa=q;}else{sam.push_back(SAM());int r=sam.size()-1;dp.push_back(0);sam[r]=sam[q];sam[r].len=sam[p].len+1;for(; p&&sam[p].nxt[ch]==q; p=sam[p].fa) sam[p].nxt[ch]=r;sam[cur].fa=sam[q].fa=r;}last=cur;
}//sam.assign(2,SAM());
//dp.assign(2,0);
//last=1

SAM的应用

求每个点 endpos 集合的大小

根据性质2和性质5,在 f a i l fail fail 树上,儿子是父亲的子集。所以我们可以直接在 f a i l fail fail 树上DP。

例题:P3804 【模板】后缀自动机(SAM)

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N=1e6+7;
struct SAM
{int fa,len;vector<int> nxt;SAM(){fa=0; len=0;nxt.assign(27,0);}
};
vector<SAM> sam;
vector<int> dp;
int last;
void insert(char c)
{int ch=c-'a';sam.push_back(SAM());int cur=sam.size()-1,p;sam[cur].len=sam[last].len+1;dp.push_back(1);for(p=last; p&&sam[p].nxt[ch]==0; p=sam[p].fa){sam[p].nxt[ch]=cur;}int q=sam[p].nxt[ch];if(q==0){sam[cur].fa=1;}else if(sam[q].len==sam[p].len+1){sam[cur].fa=q;}else{sam.push_back(SAM());int r=sam.size()-1;dp.push_back(0);sam[r]=sam[q];sam[r].len=sam[p].len+1;for(; p&&sam[p].nxt[ch]==q; p=sam[p].fa) sam[p].nxt[ch]=r;sam[cur].fa=sam[q].fa=r;}last=cur;
}
ll ans;
vector<vector<int>> e;
void dfs(int u)
{for(auto v:e[u]){dfs(v);dp[u]+=dp[v];}if(dp[u]!=1)ans=max(ans,1ll*dp[u]*sam[u].len);
}
void O_o()
{last=1;sam.assign(2,SAM());dp.assign(2,0);string s;cin>>s;for(auto c:s) insert(c);int n=sam.size()-1;e.assign(n+1,vector<int>());for(int i=1; i<=n; i++){if(sam[i].fa){e[sam[i].fa].push_back(i);}}ans=0;dfs(1);cout<<ans;
}
signed main()
{ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);cout<<fixed<<setprecision(12);int T=1;
//	cin>>T;while(T--){O_o();}
}

求不同的子串个数

一开始我们就说了个性质,你在SAM上乱走就可以走出一个子串。其实只要你走的路径不同,走出的子串一定不一样。问题就变成了从 t 0 t_0 t0 出发,总共有多少条路径。这是一个经典的 DAG 上 DP 问题。转移为
d p u = 1 + ∑ d p v dp_u=1+\sum dp_v dpu=1+dpv
最后要去掉空字符串的答案,也就是 d p 1 − 1 dp_1-1 dp11

如果是求子串个数,那就把 d p dp dp 式子换成
d p u = s i z u + ∑ d p v dp_u=siz_u+\sum dp_v dpu=sizu+dpv
其中 s i z u = ∣ e n d p o s u ∣ siz_u=|endpos_u| sizu=endposu ,还是得减掉空字符串的方案。

void dfs2(int u)
{if(dp[u]) return ;for(int i=0; i<26; i++){int v=sam[u].nxt[i];if(!v) continue;dfs2(v);dp[u]+=dp[v];}dp[u]+=sum[u];
}

当然,求本质不同的字符串总数还有另一种方法:
根据性质3
a n s = ∑ l e n i − l e n f a i ans=\sum len_{i}-len_{fa_i} ans=lenilenfai

//求每次插入后有多少本质不同的字符串
void insert(int ch)
{sam.push_back(SAM());int cur=sam.size()-1,p;sam[cur].len=sam[last].len+1;dp.push_back(1);for(p=last; p&&sam[p].nxt[ch]==0; p=sam[p].fa){sam[p].nxt[ch]=cur;}int q=sam[p].nxt[ch];if(q==0){sam[cur].fa=1;}else if(sam[q].len==sam[p].len+1){sam[cur].fa=q;}else{sam.push_back(SAM());int r=sam.size()-1;dp.push_back(0);sam[r]=sam[q];sam[r].len=sam[p].len+1;for(; p&&sam[p].nxt[ch]==q; p=sam[p].fa) sam[p].nxt[ch]=r;sam[cur].fa=sam[q].fa=r;}last=cur;ans+=sam[cur].len-sam[sam[cur].fa].len;cout<<ans<<"\n";
}

求第 k 小的子串

上一个问题中我们求出了从某个节点开始走一共有多少条路径,也就是从 i i i 开始一共有多少子串。这个时候就可以利用起来了。
如果你是在 t r i e trie trie 树上,怎么求排名第 k k k 的子串?——类似平衡树求第 k k k 小。
后缀自动机也一样,只不过每个点的 s i z e size size 不再是 1 1 1 了,而是 e n d p o s endpos endpos 集合大小。
只要把 trie 树上做的 − 1 -1 1 改成 − s i z -siz siz 即可,注意空子串还是不能算,也就是 s i z 1 = 0 siz_1=0 siz1=0

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e6+7,inf=1e18;
struct SAM
{int fa,len;vector<int> nxt;SAM(){fa=len=0;nxt.assign(26,0);}
};
int last=1;
vector<int> sum,dp;
vector<SAM> sam;
void insert(char c)
{int ch=c-'a';sam.push_back(SAM());int cur=sam.size()-1,p;sam[cur].len=sam[last].len+1;sum.push_back(1);for(p=last; p&&sam[p].nxt[ch]==0; p=sam[p].fa){sam[p].nxt[ch]=cur;}int q=sam[p].nxt[ch];if(!q){sam[cur].fa=1;}else if(sam[q].len==sam[p].len+1){sam[cur].fa=q;}else{sam.push_back(SAM());int r=sam.size()-1;sum.push_back(0);sam[r]=sam[q];sam[r].len=sam[p].len+1;for(; p&&sam[p].nxt[ch]==q; p=sam[p].fa){sam[p].nxt[ch]=r;}sam[q].fa=sam[cur].fa=r;}last=cur;
}
vector<vector<int>> e;
void dfs1(int u)
{for(auto v:e[u]){dfs1(v);sum[u]+=sum[v];}
}
void dfs2(int u)
{if(dp[u]) return ;for(int i=0; i<26; i++){int v=sam[u].nxt[i];if(!v) continue;dfs2(v);dp[u]+=dp[v];}dp[u]+=sum[u];
}
bool solve(int u,int k)
{k-=sum[u];if(k<=0) return 1;for(int i=0; i<26; i++){int v=sam[u].nxt[i];if(!v) continue;if(dp[v]>=k){cout<<char(i+'a');solve(v,k);return 1;}else k-=dp[v];}return 0;
}
void O_o()
{last=1;sam.assign(2,SAM());sum.assign(2,0);string s;cin>>s;for(auto c:s) insert(c);int n=sam.size()-1;dp.assign(n+1,0);int op,k;cin>>op>>k;if(op){e.assign(n+1,vector<int>());for(int i=1; i<=n; i++) if(sam[i].fa){e[sam[i].fa].push_back(i);}dfs1(1);}else{sum.assign(n+1,1);}dfs2(1);sum[1]=0;if(!solve(1,k)){cout<<"-1\n";return;}}
signed main()
{ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);cout<<fixed<<setprecision(12);int T=1;
//	cin>>T;while(T--){O_o();}
}

最小循环位移

循环的问题一般就是把字符串复制一遍接在后面,这个也不例外。
s ′ = s + s s'=s+s s=s+s,建立SAM
每次贪心的走最小的字符,走到长度为 s . s i z e ( ) s.size() s.size() 时就退出。

字符串出现次数

(kmp不香吗)
先预处理每个点 e n d p o s endpos endpos 集合大小
然后直接拿模式串在SAM上匹配,匹配成功就是 s i z siz siz,不成功就是 0 0 0

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 【MySQL】索引——索引的引入、认识磁盘、磁盘的组成、扇区、磁盘访问、磁盘和MySQL交互、索引的概念
  • 微信小程序 - 自定义计数器 - 优化(键盘输入校验)
  • 在VScode中导入conda环境的记录【原创】
  • 数据保险箱:SQL Server数据库备份加密的高级策略
  • 【无所从来,亦无所去】纪念去世的奶奶和外公「纪念网页」
  • 探索Python文档自动化的奥秘:MkDocs的神奇之旅
  • postgresql 字符串 替换
  • 【Linux】文件变身大作战:Linux下的文件重命名艺术
  • Spark wordcount实验
  • 探索PyCharm的C/C++支持:一站式配置指南
  • Python | Leetcode Python题解之第319题灯泡开关
  • C++ vector的基本使用(待补全)
  • Linux Vim教程
  • 探索WebKit之巅:开启现代网页应用的高效与兼容之旅
  • 强化场站网约车管理,共筑安全便捷出行新生态
  • Angular数据绑定机制
  • Java Agent 学习笔记
  • Javascript 原型链
  • javascript 总结(常用工具类的封装)
  • JAVA之继承和多态
  • JS基础之数据类型、对象、原型、原型链、继承
  • LeetCode29.两数相除 JavaScript
  • PHP的Ev教程三(Periodic watcher)
  • php面试题 汇集2
  • Puppeteer:浏览器控制器
  • PyCharm搭建GO开发环境(GO语言学习第1课)
  • SpriteKit 技巧之添加背景图片
  • ucore操作系统实验笔记 - 重新理解中断
  • 多线程 start 和 run 方法到底有什么区别?
  • - 概述 - 《设计模式(极简c++版)》
  • 关于 Cirru Editor 存储格式
  • 老板让我十分钟上手nx-admin
  • 数据仓库的几种建模方法
  • 跳前端坑前,先看看这个!!
  • 怎么把视频里的音乐提取出来
  • 基于django的视频点播网站开发-step3-注册登录功能 ...
  • ​Linux Ubuntu环境下使用docker构建spark运行环境(超级详细)
  • ​字​节​一​面​
  • # Apache SeaTunnel 究竟是什么?
  • # Java NIO(一)FileChannel
  • ### RabbitMQ五种工作模式:
  • #Datawhale AI夏令营第4期#AIGC方向 文生图 Task2
  • (2)(2.4) TerraRanger Tower/Tower EVO(360度)
  • (22)C#传智:复习,多态虚方法抽象类接口,静态类,String与StringBuilder,集合泛型List与Dictionary,文件类,结构与类的区别
  • (9)YOLO-Pose:使用对象关键点相似性损失增强多人姿态估计的增强版YOLO
  • (LeetCode 49)Anagrams
  • (Redis使用系列) Springboot 整合Redisson 实现分布式锁 七
  • (STM32笔记)九、RCC时钟树与时钟 第二部分
  • (不用互三)AI绘画:科技赋能艺术的崭新时代
  • (独孤九剑)--文件系统
  • (附源码)ssm户外用品商城 毕业设计 112346
  • (附源码)计算机毕业设计SSM基于java的云顶博客系统
  • (解决办法)ASP.NET导出Excel,打开时提示“您尝试打开文件'XXX.xls'的格式与文件扩展名指定文件不一致
  • (力扣记录)1448. 统计二叉树中好节点的数目
  • (三) diretfbrc详解