后缀自动机(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;
}
posted @ 2021-02-05 14:46  读来过倒字名把才鱼咸  阅读(122)  评论(0编辑  收藏  举报