zoukankan      html  css  js  c++  java
  • 后缀自动机(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
    ", 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
    %d
    %d
    %d
    ", 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;
    }
    
  • 相关阅读:
    Xcode4快速Doxygen文档注释 — 简明图文教程
    iOS6 旋转
    echart 判断数据是否为空
    echart tootip使用技巧
    下拉菜单自动向上或向下弹起
    前后台数据交互
    打包代码
    echart 设计宽度为百分比时,div撑不开
    无缝滚动(小鹏写)
    内置对象-Request对象
  • 原文地址:https://www.cnblogs.com/mamamoo/p/14377656.html
Copyright © 2011-2022 走看看