*在学习后缀自己主动机之前须要熟练掌握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了。

后缀自己主动机的构造

參考网上的各种模板就可以。

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvY3llbmRyYQ==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="">

后缀自己主动机的性质

裸的后缀自己主动机不过一个能够接收子串的自己主动机,在它的状态结点上维护的性质才是解题的关键。

一个构造好的 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=300000;
const int maxm=160000;
/***************
SAM 真·模板
***************/
struct State {
State *par;
State *go[52];
int val; // max。当前状态能接收的串的最长长度
int mi; // min,当前状态能接受的串的最短长度,即 par->val+1
int cnt; // 附加域,用来计数
int right; // right集,表示当前状态能够在多少个位置上出现
void init(int _val = 0){
par = 0;
val = _val;
cnt=0;
mi=0;
right=0;
memset(go,0,sizeof(go));
}
int calc(){ // 表示该状态能表示多少中不同的串
if (par==0) return 0;
return val-par->val;
}
};
State *root, *last, *cur;
State nodePool[maxn];
State* newState(int val = 0) {
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 + 1);
np->right=1; // 设置right集
while (p && p->go[w] == 0) {
p->go[w] = np;
p = p->par;
}
if (p == 0) {
np->par = root;
//total+=np->calc();
}
else {
State* q = p->go[w];
if (p->val + 1 == q->val) {
np->par = q;
//total+=np->calc();
}
else {
State* nq = newState(p->val + 1);
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=0;
memset(d,0,sizeof(d));
for (int i=1;i<cnt;i++) maxVal=max(maxVal,nodePool[i].val),d[nodePool[i].val]++;
for (int i=1;i<=maxVal;i++) d[i]+=d[i-1];
for (int i=1;i<cnt;i++) b[d[nodePool[i].val]--]=&nodePool[i];
b[0]=root;
} void gaoSamInit(){ // 求出SAM的附加信息
State* p;
int cnt=cur-nodePool;
for (int i=cnt-1;i>0;i--){
p=b[i];
p->par->right+=p->right;
p->mi=p->par->val+1;
}
} char s[maxm];
const int INF=0x3f3f3f3f;
int gao(char s[]){
int ans=INF;
int cnt=cur-nodePool;
int len=strlen(s);
int lcs=0;
State* p=root; for (int i=0;i<len;i++){
int son=s[i]-'a';
if (p->go[son]!=0){
lcs++;
p=p->go[son];
}
else{
while (p&&p->go[son]==0) p=p->par;
if (p==0){
lcs=0;
p=root;
}
else{
lcs=p->val+1;
p=p->go[son];
}
}
// TODO:
if (lcs>0) p->cnt++;
} for (int i=cnt-1;i>0;i--){
p=b[i];
// TODO:
if (p->right==1&&p->cnt==1) ans=min(ans,p->mi);
p->par->cnt += p->cnt;
}
return ans;
}

后缀自己主动机(SAM)学习指南的更多相关文章

  1. 怎样优雅的研究 RGSS3 番外(一) ruby 实现的后缀自己主动机

    *我真的不会 ruby 呀* #encoding:utf-8 #==================================================================== ...

  2. spoj 1811 LCS - Longest Common Substring (后缀自己主动机)

    spoj 1811 LCS - Longest Common Substring 题意: 给出两个串S, T, 求最长公共子串. 限制: |S|, |T| <= 1e5 思路: dp O(n^2 ...

  3. spoj 1812 LCS2 - Longest Common Substring II (后缀自己主动机)

    spoj 1812 LCS2 - Longest Common Substring II 题意: 给出最多n个字符串A[1], ..., A[n], 求这n个字符串的最长公共子串. 限制: 1 < ...

  4. Codeforces Round #244 (Div. 2)D (后缀自己主动机)

    Codeforces Round #244 (Div. 2)D (后缀自己主动机) (标号为0的节点一定是null节点,不管怎样都不能拿来用,切记切记,以后不能再错了) 这题用后缀自己主动机的话,对后 ...

  5. 浩爷AC自己主动机高速学习方案

        今天弄完自己主动机之后.从那天比赛的阴影中爬出来了,猛地一看真不咋滴难,细致一看这尼玛还不如猛的一看. ..     必备算法:KMP,字典树(KMP我写了,字典树太简单,就是一个思想.我能够 ...

  6. poj 2774 最长公共子--弦hash或后缀数组或后缀自己主动机

    http://poj.org/problem?id=2774 我想看看这里的后缀数组:http://blog.csdn.net/u011026968/article/details/22801015 ...

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

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

  8. 字符串算法之 AC自己主动机

    近期一直在学习字符串之类的算法,感觉BF算法,尽管非常easy理解,可是easy超时,全部就想学习其它的一些字符串算法来提高一下,近期学习了一下AC自己主动机.尽管感觉有所收获,可是还是有些朦胧的感觉 ...

  9. Oracle学习指南

    Oracle学习指南 你走的那天,我决定不落泪,迎着风撑着眼帘用力不眨眼 创建数据库.创建用户.创建表空间.创建表.插入数据..... 1.用系统用户登录,任选系统用户 代码: >>sql ...

随机推荐

  1. jpa自定义条件分页查询

    主要依赖 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>sp ...

  2. STM32 HAL库使用中断实现串口接收不定长数据

    以前用DMA实现接收不定长数据,DMA的方法接收串口助手的数据,全部没问题,不过如果接收模块返回的数据,而这些数据如果包含回车换行的话就会停止接收,例如接收:AT\r\nOK\r\n,就只能接收到AT ...

  3. HDU 3073 Saving Beans

    Saving Beans Time Limit: 3000ms Memory Limit: 32768KB This problem will be judged on HDU. Original I ...

  4. l洛谷 P2326 AKN’s PPAP

    P2326 AKN’s PPAP 题目描述 “I have a pen,I have an apple.Eh,Apple-Pen!. I have a pen,I have pineapple.En, ...

  5. 51 nod 1693 水群

    1693 水群 基准时间限制:0.4 秒 空间限制:524288 KB 分值: 160  难度:6级算法题  收藏  关注 总所周知,水群是一件很浪费时间的事,但是其实在水群这件事中,也可以找到一些有 ...

  6. HDU 4302 Contest 1

    维护两个优先队列即可.要注意,当出现蛋糕的位置刚好在狗的位置时,存在右边. 注意输出大小写... #include <iostream> #include <queue> #i ...

  7. 2014百度之星资格赛—— Xor Sum(01字典树)

    Xor Sum Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 132768/132768 K (Java/Others) Total ...

  8. 从100PV到1亿级PV站点架构演变

    假设你对项目管理.系统架构有兴趣,请加微信订阅号"softjg".增加这个PM.架构师的大家庭 一个站点就像一个人,存在一个从小到大的过程. 养一个站点和养一个人一样.不同一时候期 ...

  9. Unique path ii

    Follow up for "Unique Paths": Now consider if some obstacles are added to the grids. How m ...

  10. node之版本号升级和管理

    如今非常多人预计和我一样项目中已经開始应用起nodeJS,而伴随着项目的需求,对nodejs版本号也有着各种需求.好了直接进入主题,如今node版本号管理网上有非常多方式.这里说两种: 第一种modu ...