后缀自动机(SAM)
*在学习后缀自动机之前需要熟练掌握WA自动机、RE自动机与TLE自动机*
什么是后缀自动机
后缀自动机 Suffix Automaton (SAM) 是一个用 O(n) 的复杂度构造,能够接受一个字符串所有后缀的自动机。
它最早在陈立杰的 2012 年 noi 冬令营讲稿中提到。
在2013年的一场多校联合训练中,陈立杰出的 hdu 4622 可以用 SAM 轻松水过,由此 SAM 流行了起来。
一般来说,能用后缀自动机解决的问题都可以用后缀数组解决。但是后缀自动机也拥有自己的优点。
1812. Longest Common Substring II
题目大意:给出N(N <= 10)个长度不超过100000的字符串,求他们的最长公共连续子串。
时限:SPOJ上的2s
陈立杰的讲稿中用了 spoj 的1812作为例子,由于 spoj 太慢,所以只有O(n)的算法才能过掉本题,这时就要用到SAM了。
后缀自动机的构造
参考网上的各种模板即可。
后缀自动机的性质
裸的后缀自动机仅仅是一个可以接收子串的自动机,在它的状态结点上维护的性质才是解题的关键。
一个构造好的 SAM 实际上包含了两个图:由 go 数组组成的 DAG 图;由 par 指针构成的 parent 树。
SAM 的状态结点包含了很多重要的信息:
max:即代码中 val 变量,它表示该状态能够接受的最长的字符串长度。
min:表示该状态能够接受的最短的字符串长度。实际上等于该状态的 par 指针指向的结点的 val + 1。
max-min+1:表示该状态能够接受的不同的字符串数。
right:即 end-set 的个数,表示这个状态在字符串中出现了多少次,该状态能够表示的所有字符串均出现过 right 次。
par:par 指向了一个能够表示当前状态表示的所有字符串的最长公共后缀的结点。所有的状态的 par 指针构成了一个 parent 树,恰好是字符串的逆序的后缀树。
parent 树的拓扑序:序列中第i个状态的子结点必定在它之后,父结点必定在它之前。
后缀自动机的经典问题
uva 719 - Glass Beads 最小循环串
后缀自动机的遍历。
给一个字符串S,每次可以将它的第一个字符移到最后面,求这样能得到的字典序最小的字符串。
将字符串S拼接为SS,构造自动机,从根结点开始每次走最小编号的边,移动length(S)步就可以找到字典序最小的串。
由于 SAM 可以接受 SS 所有的子串,而字典序最小的字符串也必定是 SS 的子串,因此按照上面的规则移动就可以找到一个字典序最小的子串。
spoj 1811 Longest Common Substring 最长公共子串
给两个长度小于100000的字符串 A 和 B,求出他们的最长公共连续子串。
先将串 A 构造为 SAM ,然后用 B 按如下规则去跑自动机。
用一个变量 lcs 记录当前的最长公共子串,初始化为0。
设当前状态结点为 p,要匹配的字符为 c,若 go[c] 中有边,说明能够转移状态,则转移并 lcs++;
若不能转移则将状态移动到 p 的 par ,如果仍然不能转移则重复该过程直到 p 回到根节点,并将 lcs 置为 0;
如果在上一个过程中进入了能够转移的状态,则设 lcs 为当前状态的 val。
为什么失配后要移向 par 呢?因为在状态 p 上失配说明该状态的 [min,max] 所表示字符串都不是 B 中的子串,但是比它们短的后缀仍有可能是 B 的子串,而 par 指针恰好指向了该状态的后缀。
spoj 1812 Longest Common Substring II 多个串的最长公共子串
在上一题中我们知道了如何求两个串的最长公共子串,本题则是要求多个串的最长公共子串。
本题要用到 parent 树的拓扑序。
首先用第一个串构造 SAM,然后用其他的串匹配它。
SAM 的状态要多维护两个信息:lcs,当多个串的最长公共子串的最后一个字符落在该状态上的长度;nlcs,当前串的最长公共子串的最后一个字符落在该状态上的长度。
我们对每个串的匹配之后,要对每个状态的 lcs 进行维护,显然 lcs=min(lcs, nlcs),而我们最后所求的就是所有状态中 lcs 的最大值。
匹配的过程与上一题相同,但是在匹配过程中,到达状态 p 时得到的 nlcs 未必就是该状态能表示的最长公共子串长,因为如果一个子串出现了n次,那么子串的所有后缀也至少出现了n次。
因此在每个串匹配之后求要按照拓扑序的逆序维护每个状态的 nlcs,使 p->par->nlcs=max(p->nlcs, p->par->nlcs)。
hdu 4622 Reincarnation 统计不同子串个数
这也是许多新人第一次接触到 SAM 的题。本题可以用各种姿势 AC,但是用 SAM 最轻松。
给出一个字符串,最长2000,q个询问,每次询问[l,r]区间内有多少个不同的字串。
SAM 中的每个状态能够表示的不同子串的个数为 val - 父结点的 val。因此在构造自动机时,用变量 total 记录当前自动机能够表示的不同子串数,对每一次 extend 都更新 total 的值。将这个过程中的每一个 total 值都记录下了就能得到一个表示子串个数表。我们对字符串的每一个后缀都重新构造一遍 SAM 就可以得到一个二维的表。
对每次询问,在表中查找相应的值即可。
hdu 4436 str2int 处理不同的子串
给出n个数字,数字很长,用字符串读入,长度总和为10^5。求这n个字符串的所有子串(不重复)的和取模2012 。
题目要对所有不重复的子串进行处理,考虑使用 SAM 来将解决。
将 n 个数字拼接成一个字符串,用不会出现的数字 10 进行分割。
构造完之后按照拓扑序计算每个状态上的 sum 与 cnt,sum 表示以当前状态为结尾的子串的和,cnt 表示有多少种方法到达当前结点。
设父结点为 u 向数字 k 移动到的子结点为 v, 显然结点 v 的状态要在 sum 上增加 add=u->sum*10+u->cnt*k。即 u 的能表示的数字总和乘上10再加上到达 v 的方法总数乘上当前的个位数字 k。
最后答案就是将所有状态的 sum 求和。
spoj 8222 Substrings 子串出现次数
给一个字符串S,令F(x)表示S的所有长度为x的子串中,出现次数的最大值。求F(1)..F(Length(S)) 。
在拓扑序的逆序上维护每个状态的 right,表示当前状态的出现次数。
最后当前用每个状态的 right 来更新 f[val],即当前状态能表示的最长串的出现次数。
最后用 f[i] 依次去更新 f[i-1] 取最大值,因为若一个长度为 i 的串出现了 f[i] 次,那么长度为 i-1 的串至少出现 f[i] 次。
poj 3415Common Substrings 子串计数
给出两个串,问这两个串的所有的子串中(重复出现的,只要是位置不同就算两个子串),长度大于等于k的公共子串有多少个。
先对第一个串构造 SAM,通过状态的 right 与 val 可以轻松求出它能表示的所有子串数。现在的问题是如何满足条件。
用第二个串对 SAM 做 LCS,当前状态 LCS >= K 时,维护状态上的 cnt++,表示该状态为大于K且最长公共串的结尾的次数为 cnt 次。
统计最长公共子串的状态中满足条件的个数 ans+=(lcs-max(K,p->mi)+1)*p->right
匹配结束后,用拓扑序的逆序维护每个状态父结点 cnt,此时 cnt 的含义为该状态被包含的次数。
统计不是最长公共子串的状态但是被子串包含的个数,ans+=p->cnt*(p->par->val - max(K,p->par->mi)+1)*p->par->right,用父结点被包含的次数乘以满足条件的串数累加到答案中。
spoj 7258 Lexicographical Substring Search 求字典序
给出一个字符串,长度为90000。询问q次,每次回答一个k,求字典序第k小的子串。
仍然用拓扑序得到每个状态拥有的不同子串数。
对第k小的子串,按字典序枚举边,跳过一条边则 k 减去该边指向的状态的不同子串数,直到不能跳过,然后沿着该边移动一次,循环这个步骤直到 k变为0。
此时的路径就是字典序第k小的子串。
Codeforces 235C Cyclical Quest 串的出现次数
*这场比赛的出题人是 WJMZBMR 陈立杰*
给出一个字符串s,这里称之为母串,然后再给出n个子串,n<=10^5,子串长度总和不超过10^6。问,对于每一个子串的所有不同的周期性的同构串在母串中出现的次数总和。
将母串构造 SAM,将子串复制拼接到一起然后去掉最后一个字母去跑 SAM。
对满足条件的状态向上维护直到原子串的长度包含在了状态能表示的长度中并用 mark 标记。
然后将该状态的出现次数累加到答案上,如果一个应该累加的状态已经被 mark 过了,就不再累加。
Codeforces 427D Match & Catch 公共串的出现次数
给出两个长度均不超过5000的字符串s1,s2,求这两个串中,都只出现一次的最短公共子串。
对第一个串构造 SAM,用第二个串跑。显然 right 为1的状态就是在第一个串中出现次数为1的子串。
匹配过程总的每进入一个结点,就将结点上的 cnt 加一,表示该状态表示的最长公共串在第二个串的出现次数。
最后按拓扑序逆序求出所有状态的 cnt,若一个结点出现过 cnt 次,那么他的父结点即它的后缀出现次数也要加上 cnt。
最后遍历所有的状态,right 等于 1 且 cnt 等于 1 的状态就是出现次数为1的公共子串,找到其中最短的作为答案即可。
我的板子
#include <iostream>
#include <cstring>
#include <cstdio> using namespace std;
typedef long long LL;
const int maxn=;
const int maxm=;
/***************
SAM 真·模板
***************/
struct State {
State *par;
State *go[];
int val; // max,当前状态能接收的串的最长长度
int mi; // min,当前状态能接受的串的最短长度,即 par->val+1
int cnt; // 附加域,用来计数
int right; // right集,表示当前状态可以在多少个位置上出现
void init(int _val = ){
par = ;
val = _val;
cnt=;
mi=;
right=;
memset(go,,sizeof(go));
}
int calc(){ // 表示该状态能表示多少中不同的串
if (par==) return ;
return val-par->val;
}
};
State *root, *last, *cur;
State nodePool[maxn];
State* newState(int val = ) {
cur->init(val);
return cur++;
}
//int total; // 不同的子串个数。
void initSAM() {
//total = 0;
cur = nodePool;
root = newState();
last = root;
}
void extend(int w) {
State* p = last;
State* np = newState(p->val + );
np->right=; // 设置right集
while (p && p->go[w] == ) {
p->go[w] = np;
p = p->par;
}
if (p == ) {
np->par = root;
//total+=np->calc();
}
else {
State* q = p->go[w];
if (p->val + == q->val) {
np->par = q;
//total+=np->calc();
}
else {
State* nq = newState(p->val + );
memcpy(nq->go, q->go, sizeof(q->go));
//total -= q->calc();
nq->par = q->par;
q->par = nq;
np->par = nq;
//total += q->calc()+nq->calc()+np->calc();
while (p && p->go[w] == q) {
p->go[w] = nq;
p = p->par;
}
}
}
last = np;
} int d[maxm];
State* b[maxn];
void topo(){ // 求出parent树的拓扑序
int cnt=cur-nodePool;
int maxVal=;
memset(d,,sizeof(d));
for (int i=;i<cnt;i++) maxVal=max(maxVal,nodePool[i].val),d[nodePool[i].val]++;
for (int i=;i<=maxVal;i++) d[i]+=d[i-];
for (int i=;i<cnt;i++) b[d[nodePool[i].val]--]=&nodePool[i];
b[]=root;
} void gaoSamInit(){ // 求出SAM的附加信息
State* p;
int cnt=cur-nodePool;
for (int i=cnt-;i>;i--){
p=b[i];
p->par->right+=p->right;
p->mi=p->par->val+;
}
} char s[maxm];
const int INF=0x3f3f3f3f;
int gao(char s[]){
int ans=INF;
int cnt=cur-nodePool;
int len=strlen(s);
int lcs=;
State* p=root; for (int i=;i<len;i++){
int son=s[i]-'a';
if (p->go[son]!=){
lcs++;
p=p->go[son];
}
else{
while (p&&p->go[son]==) p=p->par;
if (p==){
lcs=;
p=root;
}
else{
lcs=p->val+;
p=p->go[son];
}
}
// TODO:
if (lcs>) p->cnt++;
} for (int i=cnt-;i>;i--){
p=b[i];
// TODO:
if (p->right==&&p->cnt==) ans=min(ans,p->mi);
p->par->cnt += p->cnt;
}
return ans;
}
SAM模板
后缀自动机(SAM)的更多相关文章
- [转]后缀自动机(SAM)
原文地址:http://blog.sina.com.cn/s/blog_8fcd775901019mi4.html 感觉自己看这个终于觉得能看懂了!也能感受到后缀自动机究竟是一种怎样进行的数据结构了. ...
- 【算法】后缀自动机(SAM) 初探
[自动机] 有限状态自动机的功能是识别字符串,自动机A能识别字符串S,就记为$A(S)$=true,否则$A(S)$=false. 自动机由$alpha$(字符集),$state$(状态集合),$in ...
- SPOJ 1811. Longest Common Substring (LCS,两个字符串的最长公共子串, 后缀自动机SAM)
1811. Longest Common Substring Problem code: LCS A string is finite sequence of characters over a no ...
- 后缀自动机SAM学习笔记
前言(2019.1.6) 已经是二周目了呢... 之前还是有一些东西没有理解到位 重新写一下吧 后缀自动机的一些基本概念 参考资料和例子 from hihocoder DZYO神仙翻译的神仙论文 简而 ...
- 浅谈后缀自动机SAM
一下是蒟蒻的个人想法,并不很严谨,仅供参考,如有缺误,敬请提出 参考资料: 陈立杰原版课件 litble 某大神 某大神 其实课件讲得最详实了 有限状态自动机 我们要学后缀自动机,我们先来了解一下自动 ...
- 后缀自动机(SAM)奶妈式教程
后缀自动机(SAM) 为了方便,我们做出如下约定: "后缀自动机" (Suffix Automaton) 在后文中简称为 SAM . 记 \(|S|\) 为字符串 \(S\) 的长 ...
- 【算法】后缀自动机(SAM) 例题
算法介绍见:http://www.cnblogs.com/Sakits/p/8232402.html 广义SAM资料:https://www.cnblogs.com/phile/p/4511571.h ...
- 后缀自动机(SAM)速成手册!
正好写这个博客和我的某个别的需求重合了...我就来讲一讲SAM啦qwq 后缀自动机,也就是SAM,是一种极其有用的处理字符串的数据结构,可以用于处理几乎任何有关于子串的问题,但以学起来异常困难著称(在 ...
- 【算法专题】后缀自动机SAM
后缀自动机是用于识别子串的自动机. 学习推荐:陈立杰讲稿,本文记录重点部分和感性理解(论文语言比较严格). 刷题推荐:[后缀自动机初探],题目都来自BZOJ. [Right集合] 后缀自动机真正优于后 ...
- 【文文殿下】对后缀自动机(SAM)的理解
后缀自动机,是一种数据结构,是由状态和转移关系构成的.它虽然叫做后缀自动机,可是他却与后缀并没有什么太大的联系. 后缀自动机的每一种状态都是原串的一些子串的集合,每个子串只唯一存在于某个状态中,对每一 ...
随机推荐
- UVA 10716 Evil Straw Warts Live(贪心)
Problem D: Evil Straw Warts Live A palindrome is a string of symbols that is equal to itself when re ...
- 【HDOJ】1031 Design T-Shirt
qsort直接排序. #include <stdio.h> #include <string.h> #include <stdlib.h> #define MAXN ...
- 【HDOJ】1983 Kaitou Kid - The Phantom Thief (2)
不仅仅是DFS,还需要考虑可以走到终点.同时,需要进行预处理.至多封闭点数为起点和终点的非墙壁点的最小值. #include <iostream> #include <cstdio& ...
- Android-获取外置SDcard路径
Android手机支持SDcard.目前很多手机厂商把SDcard集成到手机中,当然有的手机同时也支持可插拔的SDcard.这就有了内置SDcard和位置SDcard之分.当手机同时支持内置和外置SD ...
- 畅通工程 HDOJ--1863
畅通工程 Time Limit: 1000/1000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others)Total Submis ...
- Linux创建新用户以及useradd adduser的区别
从阿里云那弄了个机子玩玩,系统用的是Ubuntu12.04.刚等上去时候是用root登录的,首先想到的就是创建一个用户. 使用 useradd myname 发现/home目录下没有myname的家目 ...
- HDOJ/HDU 2352 Verdis Quo(罗马数字与10进制数的转换)
Problem Description The Romans used letters from their Latin alphabet to represent each of the seven ...
- Javascript 母羊生小羊问题,递归
农场买了一只小羊,这种羊在第一年是小羊,第二年的年底会生一只小羊,第三年不生小羊,第四年的年底还会再生下一只小羊,第五年就死掉了. 要计算N年时农场里有几只羊. [凡是碰到“一生二.二生三.三生万物” ...
- Java中url传递中文参数取值乱码的解决方法
java中URL参数中有中文值,传到服务端,在用request.getParameter()方法,得到的常常会是乱码,这将涉及到字符解码操作. 方法一: http://xxx.do?ptname=’我 ...
- 报错:Cannot insert explicit value for identity column in table 't' when identity_insert is set to OFF
通常情况下,不能向 SQL Server 自增字段插入值,如果非要这么干的话,SQL Server 就会好不客气地给你个错误警告: Server: Msg 544, Level 16, State 1 ...