后缀自动机(SAM)构造实现过程演示+习题集锦
蒟蒻写这篇\(blog\)主要是存一下,后缀自动机的详细搭建过程,方便以后复习
具体的某些证明,为什么这么做,正确性劈里啪啦一大堆就不赘述了讲解指路☞
后缀自动机
后缀自动机上每一条到\(i\)的路径对应一个子串,整个自动机包含了字符串的所有子串
很多时候可以和后缀数组等价使用
\(endpos\):一个子串\(i\)在整个字符串中出现的位置 最后一个字符的下标 构成的集合
举个栗子 \(abcbcdeabc\),(从\(0\)开始标号)
子串\(abc\)对应的\(endpos\)为\(\{2,9\}\),子串\(bc\)的\(endpos\)为\(\{2,4,9\}\)
后缀自动机的编号对应的就是\(endpos\)完全相同的所有子串
依旧是上面的粒子\(abcbcdeabc\)
子串\(bc\)的\(endpos\)为\(\{2,4,9\}\),子串\(c\)的\(endpos\)也为\(\{2,4,9\}\)
那么后缀自动机上对应的\(id\)编号既表示\(bc\)子串,也表示\(c\)子串
算法实现过程
- \(e.g.1\),构建\(abcd\)的后缀自动机
Ⅰ最初始状态,仅有一个空根,\(last=1\),\(last\)表示后缀自动机的最后一个节点
Ⅱ 将\('a'\)扔进去,新建一个节点\(cnt=2\),\(len=len[last]+1=1\)
从\(last\)开始跳,发现\(1\)没有\('a'\)边
则建立一条\('a'\)边,并指向新点\(2\)
此时跳到了初始源点,\(2\)的后缀链接只能指向\(1\),\(last\)变为\(2\)
Ⅲ 将\('b'\)扔进去,新建一个节点\(cnt=3,len=len[last]+1=2\)
从\(last\)开始跳后缀链接
\(2\)没有\('b'\)边,新建一条并指向\(3\),跳后缀链接到\(1\)
\(1\)没有\('b'\)边,新建一条并指向\(3\)
此时已经到了根节点,\(3\)的后缀链接只能指向\(1\),\(last=3\)
Ⅳ 将\('c'\)扔进去,新建一个节点\(cnt=4,len=3\)
从\(last\)开始跳后缀链接
\(3\)没有\('c'\)边,新建一条并指向\(4\),跳后缀链接到\(1\)
\(1\)没有\('c'\)边,新建一条并指向\(4\)
此时已经到了根节点,\(4\)的后缀链接只能指向\(1\),\(last=4\)
Ⅴ 将\('d'\)扔进去,新建一个节点\(cnt=5,len=4\)
从\(last\)开始跳后缀链接
\(4\)没有\('c'\)边,新建一条并指向\(5\),跳后缀链接到\(1\)
\(1\)没有\('c'\)边,新建一条并指向\(5\)
此时已经到了根节点,\(5\)的后缀链接只能指向\(1\),\(last=5\)
最简单的一种后缀自动机就完成了
接下来就尝试一下进阶版
- \(e.g.2\),构建\(ababe\)的后缀自动机
Ⅰ先搭建空源点,\(last=1\)
Ⅱ 加入\('a'\),新建一个节点\(cnt=2,len[2]=len[last]+1=1\)
\(1\)没有\('c'\)边,新建一条并指向\(2\),
此时已经到了根节点,\(2\)的后缀链接只能指向\(1\),\(last=2\)
Ⅲ 加入\('b'\),新建一个节点\(cnt=3,len[3]=len[last]+1=2\)
从\(last\)开始跳后缀链接
\(2\)没有\('b'\)边,新建一条并指向\(3\),跳后缀链接到\(1\)
\(1\)没有\('b'\)边,新建一条并指向\(3\)
此时已经到了根节点,\(3\)的后缀链接只能指向\(1\),\(last=3\)
Ⅳ 再加入\('a'\),新建一个节点\(cnt=4,len[4]=len[last]+1=3\)
从\(last\)开始跳后缀链接
\(3\)没有\('a'\)边,新建一条并指向\(4\),跳后缀链接到\(1\)
\(1\)有一条指向\(2\)的\('a'\)边,满足\(len[2]=len[1]+1\),则直接将\(4\)后缀链接指向\(2\)
结束,\(last=4\)
Ⅴ 再加入\('b'\),新建一个节点\(cnt=5,len[5]=len[last]+1=4\)
从\(last\)开始跳后缀链接
\(4\)没有\('b'\)边,新建一条并指向\(5\),跳后缀链接到\(2\)
\(2\)有一条指向\(3\)的\('b'\)边,满足\(len[3]=len[2]+1\),直接将\(5\)后缀链接指向\(3\)
结束,\(last=5\)
Ⅵ 加入新\('c'\),新建一个节点\(cnt=6,len[6]=len[last]+1=5\)
从\(last\)开始跳后缀链接
\(5\)没有\('c'\)边,新建一条并指向\(6\),跳后缀链接到\(3\)
\(3\)没有\('c'\)边,新建一条并指向\(6\),跳后缀链接到\(1\)
\(1\)没有\('c'\)边,新建一条并指向\(6\)
此时已到根节点,\(6\)只能链接\(1\),\(last=6\)结束
这就是进阶版了,没有涉及到最终版的点复制
最后让我们一起携手走进最终版的后缀自动机构造
- \(e.g.3\),构建\(cabab\)的后缀自动机
Ⅰ 创造新源点,\(last=1,cnt=1\)
Ⅱ 加入\('c'\),新建一个节点\(cnt=2,len[2]=len[last]+1=1\)
从\(last\)开始跳后缀链接
\(1\)没有\('c'\)边,新建一条并指向\(2\)
此时已到根节点,\(2\)只能链接\(1,last=2\)
Ⅲ 加入\('a'\),新建一个节点\(cnt=3,len[3]=len[last]+1=2\)
从\(last\)开始跳后缀链接
\(2\)没有\('a'\)边,新建一条并指向\(3\),跳后缀链接到\(1\)
\(1\)没有\('a'\)边,新建一条并指向\(3\)
此时已到根节点,\(3\)只能链接\(1,last=3\)
Ⅳ 加入\('b'\),新建一个节点\(cnt=4,len[4]=len[last]+1=3\)
从\(last\)开始跳后缀链接
\(3\)没有\('b'\)边,新建一条并指向\(4\),跳后缀链接到\(1\)
\(1\)没有\('a'\)边,新建一条并指向\(4\)
此时已到根节点,\(4\)只能链接\(1,last=4\)
Ⅴ 加入\('a'\),新建一个节点\(cnt=5,len[5]=len[last]+1=4\)
从\(last\)开始跳后缀链接
\(4\)没有\('a'\)边,新建一条并指向\(5\),跳后缀链接到\(1\)
\(1\)有\('a'\)边,指向\(3\),但是!!!\(len[3]≠len[1]+1\),不能像进阶版直接链接,这里必须要点复制
新建一个\(3\)的分身节点\(cnt=6\)
\(3\)的所有信息(出入边)除了原字符串间的边(图中黑色边)全部修改为分点\(6\)的边,直接覆盖
并且\(6\)成为\(3\)的直接后缀链接,替代\(1\)
\(len[6]=len[1]+1=1\)
相当于\(6\)做了\(1,3\)后缀链之间的承接点,保证了每一条边上\(len\)只会带来\(+1\)的影响
\(5\)直接链接\(6\)后结束,\(last=5\)
Ⅵ 加入\('b'\),新建节点\(cnt=7\)
从\(last\)开始跳后缀链接
\(5\)没有\('b'\)边,新建一条指向\(7\),跳后缀链接到\(6\)
\(6\)有一条\('b'\)边,指向\(4\),判断\(len[4]≠len[6]+1\)
再次执行复制操作
新建一个\(4\)的分身节点\(cnt=8\)
\(4\)的所有信息(出入边)除了原字符串间的边(图中黑色边)全部修改为分点\(8\)的边,直接进行覆盖
\(8\)成为\(4\)的直接后缀链接,\(len[8]=len[6]+1=2\)
\(7\)直接链接\(8\)后结束,\(last=7\)
⚡
\(len[x]\)复制点的\(len\)不等于被复制点的原后缀链接的\(len+1\),而是谁触发的\(len+1\)
模板
struct node {
int len; //长度
int fa; //后缀链接
int son[maxc]; //字符集大小
}t[maxn];
模拟从主链的前一个开始跳后缀链接,并对于链接上的没有该字符边的每一个点都连出一条新字符边
while( pre && ! t[pre].son[c] ) t[pre].son[c] = now, pre = t[pre].fa;
跳到根,代表这是首个出现的字符,他只能链接最初的根节点了
if( ! pre ) t[now].fa = 1;
否则,如果路上找到了,满足\(len\)的关系,直接后缀链接指过去即可
int u = t[pre].son[c];
if( t[u].len == t[pre].len + 1 ) t[now].fa = u;
复制该点,并进行有关该点的所有信息重改
①原点连出的点,新点也要连出
②连入原点的点,变成连入新点
③原点和新点间也需建立联系,新点是原点的后缀链接
else {
int v = ++ tot;
t[v] = t[u];//利用结构体巧妙将原点连出的点进行复制
t[v].len = t[pre].len + 1;//由谁触发 len就是触发点len+1
t[u].fa = t[now].fa = v;//原点与复制点与新建点的关系
while( pre && t[pre].son[c] == u ) t[pre].son[c] = v, pre = t[pre].fa;//暴力复制修改连入原点的点
}
习题
洛谷后缀自动机模板题
- code
#include <cstdio>
#include <vector>
#include <cstring>
using namespace std;
#define maxn 2000005
vector < int > G[maxn];
struct node {
int fa, len;
int son[30];
}t[maxn];
char s[maxn];
int last = 1, tot = 1;
long long ans;
int siz[maxn];
void insert( int c ) {
int pre = last, now = last = ++ tot;
siz[tot] = 1;
t[now].len = t[pre].len + 1;
while( pre && ! t[pre].son[c] ) t[pre].son[c] = now, pre = t[pre].fa;
if( ! pre ) t[now].fa = 1;
else {
int u = t[pre].son[c];
if( t[u].len == t[pre].len + 1 ) t[now].fa = u;
else {
int v = ++ tot;
t[v] = t[u];
t[v].len = t[pre].len + 1;
t[u].fa = t[now].fa = v;
while( pre && t[pre].son[c] == u ) t[pre].son[c] = v, pre = t[pre].fa;
}
}
}
void dfs( int u ) {
for( int i = 0;i < G[u].size();i ++ ) {
int v = G[u][i];
dfs( v );
siz[u] += siz[v];
}
if( siz[u] != 1 ) ans = max( ans, 1ll * siz[u] * t[u].len );
}
int main() {
scanf( "%s", s );
int len = strlen( s );
for( int i = 0;i < len;i ++ ) insert( s[i] - 'a' );
for( int i = 2;i <= tot;i ++ ) G[t[i].fa].push_back( i );
dfs( 1 );
printf( "%lld", ans );
return 0;
}
品酒大会
- solution
有一个\(SAM\)常用结论:前缀\(i,j\)的最长公共后缀\(=parent\ tree\)上前缀\(i,j\)分别指向的点\(u,v\)的\(lca\)反映在后缀自动机上的节点代表的最长子串
将本题的字符串倒过来建后缀自动机,在自动机上进行树上\(dp\),最后从后往前进行更新即可 - code
#include <cstdio>
#include <vector>
#include <cstring>
#include <iostream>
using namespace std;
#define inf 0x7f7f7f7f
#define int long long
#define maxn 600005
struct node {
int len, fa;
int son[30];
}t[maxn];
vector < int > G[maxn];
int last = 1, cnt = 1, n;
char s[maxn];
int a[maxn], f[maxn], tot[maxn], siz[maxn], maxx[maxn], minn[maxn];
void insert( int x, int w ) {
int pre = last, now = last = ++ cnt;
siz[now] = 1, t[now].len = t[pre].len + 1;
maxx[now] = minn[now] = w;
while( pre && ! t[pre].son[x] ) t[pre].son[x] = now, pre = t[pre].fa;
if( ! pre ) t[now].fa = 1;
else {
int u = t[pre].son[x];
if( t[u].len == t[pre].len + 1 ) t[now].fa = u;
else {
int v = ++ cnt;
maxx[v] = -inf, minn[v] = inf;
t[v] = t[u];
t[v].len = t[pre].len + 1;
t[u].fa = t[now].fa = v;
while( pre && t[pre].son[x] == u ) t[pre].son[x] = v, pre = t[pre].fa;
}
}
}
bool check( int u ) {
return maxx[u] != -inf && minn[u] != inf;
}
void dfs( int u ) {
for( int i = 0;i < G[u].size();i ++ ) {
int v = G[u][i];
dfs( v );
tot[t[u].len] += siz[u] * siz[v];
siz[u] += siz[v];
if( check( u ) )
f[t[u].len] = max( f[t[u].len], max( maxx[u] * maxx[v], minn[u] * minn[v] ) );
maxx[u] = max( maxx[u], maxx[v] );
minn[u] = min( minn[u], minn[v] );
}
}
signed main() {
memset( f, -0x7f, sizeof( f ) );
scanf( "%d %s", &n, s + 1 );
for( int i = 1;i <= n;i ++ )
scanf( "%lld", &a[i] );
for( int i = n;i;i -- ) insert( s[i] - 'a', a[i] );
for( int i = 2;i <= cnt;i ++ ) G[t[i].fa].push_back( i );
dfs( 1 );
for( int i = n - 1;~ i;i -- ) tot[i] += tot[i + 1], f[i] = max( f[i], f[i + 1] );
for( int i = 0;i < n;i ++ )
printf( "%lld %lld\n", tot[i], ( tot[i] ? f[i] : 0 ) );
return 0;
}
[HEOI2015]最短不公共子串
- solution
做此题需要了解序列自动机
然后就是很无脑的四个\(bfs\)跑
子串就是跑后缀自动机
子序列就是跑序列自动机 - code
#include <queue>
#include <cstdio>
#include <cstring>
using namespace std;
#define maxn 5000
char a[maxn], b[maxn];
struct SAM {
struct node {
int len, fa;
int son[30];
}t[maxn];
int last, cnt;
SAM() {
last = cnt = 1;
}
void insert( int x ) {
int pre = last, now = last = ++ cnt;
t[now].len = t[pre].len + 1;
while( pre && ! t[pre].son[x] ) t[pre].son[x] = now, pre = t[pre].fa;
if( ! pre ) t[now].fa = 1;
else {
int u = t[pre].son[x];
if( t[u].len == t[pre].len + 1 ) t[now].fa = u;
else {
int v = ++ cnt;
t[v] = t[u];
t[v].len = t[pre].len + 1;
t[u].fa = t[now].fa = v;
while( pre && t[pre].son[x] == u ) t[pre].son[x] = v, pre = t[pre].fa;
}
}
}
}SamA, SamB;
struct SEQ {
int nxt[maxn][30], last[30];
SEQ() {
memset( nxt, 0, sizeof( nxt ) );
memset( last, 0, sizeof( last ) );
}
void build( int n, char *s ) {
for( int i = n;~ i;i -- ) {
for( int j = 0;j < 26;j ++ )
if( last[j] ) nxt[i + 1][j] = last[j];
if( i ) last[s[i] - 'a'] = i + 1;
}
}
}SeqA, SeqB;
struct node {
int x, y, dep;
node(){}
node( int X, int Y, int Dep ) {
x = X, y = Y, dep = Dep;
}
};
queue < node > q;
bool vis[maxn][maxn];
void init() {
memset( vis, 0, sizeof( vis ) );
while( ! q.empty() ) q.pop();
vis[1][1] = 1, q.push( node( 1, 1, 0 ) );
}
int bfs1() {
init();
while( ! q.empty() ) {
node now = q.front(); q.pop();
for( int i = 0;i < 26;i ++ ) {
int sonA = SamA.t[now.x].son[i];
int sonB = SamB.t[now.y].son[i];
if( vis[sonA][sonB] ) continue;
else if( sonA && ! sonB ) return now.dep + 1;
else if( sonA && sonB ) {
vis[sonA][sonB] = 1;
q.push( node( sonA, sonB, now.dep + 1 ) );
}
}
}
return -1;
}
int bfs2() {
init();
while( ! q.empty() ) {
node now = q.front(); q.pop();
for( int i = 0;i < 26;i ++ ) {
int sonA = SamA.t[now.x].son[i];
int sonB = SeqB.nxt[now.y][i];
if( vis[sonA][sonB] ) continue;
else if( sonA && ! sonB ) return now.dep + 1;
else if( sonA && sonB ) {
vis[sonA][sonB] = 1;
q.push( node( sonA, sonB, now.dep + 1 ) );
}
}
}
return -1;
}
int bfs3() {
init();
while( ! q.empty() ) {
node now = q.front(); q.pop();
for( int i = 0;i < 26;i ++ ) {
int sonA = SeqA.nxt[now.x][i];
int sonB = SamB.t[now.y].son[i];
if( vis[sonA][sonB] ) continue;
else if( sonA && ! sonB ) return now.dep + 1;
else if( sonA && sonB ) {
vis[sonA][sonB] = 1;
q.push( node( sonA, sonB, now.dep + 1 ) );
}
}
}
return -1;
}
int bfs4() {
init();
while( ! q.empty() ) {
node now = q.front(); q.pop();
for( int i = 0;i < 26;i ++ ) {
int sonA = SeqA.nxt[now.x][i];
int sonB = SeqB.nxt[now.y][i];
if( vis[sonA][sonB] ) continue;
else if( sonA && ! sonB ) return now.dep + 1;
else if( sonA && sonB ) {
vis[sonA][sonB] = 1;
q.push( node( sonA, sonB, now.dep + 1 ) );
}
}
}
return -1;
}
int main() {
scanf( "%s %s", a + 1, b + 1 );
int lena = strlen( a + 1 ), lenb = strlen( b + 1 );
for( int i = 1;i <= lena;i ++ )
SamA.insert( a[i] - 'a' );
for( int i = 1;i <= lenb;i ++ )
SamB.insert( b[i] - 'a' );
SeqA.build( lena, a );
SeqB.build( lenb, b );
printf( "%d\n%d\n%d\n%d\n", bfs1(), bfs2(), bfs3(), bfs4() );
return 0;
}
字符串
- solution
这题运用的思想主要是广义后缀自动机,即将多个字符串建在一个后缀自动机上
其实并没有什么新颖之处,只需在扩展的时候带一个这个字符属于哪个字符串的编号即可
假设已经建好了自动机,接下来考虑两个长度为\(k\)的子串之间如何一一对应修改
这个时候如果将其放到\(parent\ tree\)上考虑的话,就简单了
其实可以猜想一下,刚开始我就想到了虚树的性质,即相邻两两配对
不难证明,的确应该相邻两个不同属类的子串配对
前缀\(i,j\)的最长公共后缀\(=parent\ tree\)上前缀\(i,j\)分别指向的点\(u,v\)的\(lca\)反映在后缀自动机上的节点代表的最长子串
也就是最后变成深搜一棵树的模样,记得特判可能\(lca\)代表的最长子串长度\(\ge k\)
此时是不需要代价的
- code
#include <cstdio>
#include <vector>
#include <iostream>
#include <algorithm>
using namespace std;
#define maxn 600005
struct node {
int len, fa;
int son[30];
}t[maxn];
vector < int > G[maxn];
int n, k, last = 1, cnt = 1;
long long ans;
char a[maxn], b[maxn];
int type[maxn];
int tot[maxn][3];
void insert( int x, int s, int pos ) {
int pre = last, now = last = ++ cnt;
t[now].len = t[pre].len + 1;
while( pre && ! t[pre].son[x] ) t[pre].son[x] = now, pre = t[pre].fa;
if( ! pre ) t[now].fa = 1;
else {
int u = t[pre].son[x];
if( t[u].len == t[pre].len + 1 ) t[now].fa = u;
else {
int v = ++ cnt;
t[v] = t[u];
t[v].len = t[pre].len + 1;
t[u].fa = t[now].fa = v;
while( pre && t[pre].son[x] == u ) t[pre].son[x] = v, pre = t[pre].fa;
}
}
if( pos >= k ) type[now] = s;
}
void dfs( int u ) {
tot[u][type[u]] ++;
for( int i = 0;i < G[u].size();i ++ ) {
int v = G[u][i];
dfs( v );
tot[u][1] += tot[v][1];
tot[u][2] += tot[v][2];
}
if( tot[u][1] >= tot[u][2] ) {
int x = max( 0, k - t[u].len );
ans += 1ll * x * tot[u][2];
tot[u][1] -= tot[u][2];
tot[u][2] = 0;
}
else {
int x = max( 0, k - t[u].len );
ans += 1ll * x * tot[u][1];
tot[u][2] -= tot[u][1];
tot[u][1] = 0;
}
}
int main() {
scanf( "%d %d %s %s", &n, &k, a + 1, b + 1 );
reverse( a + 1, a + n + 1 );
reverse( b + 1, b + n + 1 );
for( int i = 1;i <= n;i ++ ) insert( a[i] - 'a', 1, i );
for( int i = 1;i <= n;i ++ ) insert( b[i] - 'a', 2, i );
for( int i = 2;i <= cnt;i ++ ) G[t[i].fa].push_back( i );
dfs( 1 );
printf( "%lld", ans );
return 0;
}