首页 > 代码库 > 后缀自己主动机(SAM)学习指南

后缀自己主动机(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=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)学习指南