后缀自动机详解

前言

\(3\)次尝试学习后缀自动机……下定决心不再背板子

参考资料:

洛谷博客(KesdiaelKen的雷蒻论坛)

2012年noi冬令营clj讲稿

前置知识:Trie树

简介

后缀自动机,顾名思义是能识别所有后缀的自动机。那么就要从两个方面入手:什么是自动机,以及怎样让自动机识别所有后缀。

其实识别所有后缀用Trie树就可以做到,把所有后缀插到Trie里即可。但是当点数比较多的时候,Trie树\(O(n^2)\)的时间和空间复杂度就吃不消了。于是就需要后缀自动机。

自动机

有限状态自动机能识别字符串。自动机由\(5\)个部分组成,分别是字符集\(Alpha\),状态集合\(State\),初始状态\(Init\),结束状态集合\(End\),和状态转移函数\(Trans\)。如果一个自动机\(A\)能识别字符串\(S\),那么记\(A(S)=1(true)\),否则\(A(S)=0(false)\)

定义\(Trans(R\in \{State\cup \{NULL\}\},ch/str)\)为状态\(R\)后加字符\(ch\)或字符串\(str\)所达到的状态。如果不存在则记为\(NULL\)

容易发现自动机\(A\)能识别的串就是所有的\(S\)使得\(Trans(Init,S)\in End\)。记\(Reg(A)=\{S:Trans(Init,S)\in End\}\)

后缀自动机概念

由上面自动机的概念,我们可以知道,一个\(String\)的SAM(suffix automaton),\(S\in Reg(SAM)\)当且仅当\(S\)\(String\)的后缀。然而似乎Trie仍然可以满足……我们需要压缩Trie的状态!

下面不妨令\(String="aabab"\)\(n\)\(Length(String)\)

我们定义\(String\)的子串\(S\)出现位置的右端点集合为\(EndPos(S)\)。例如例子中\(EndPos("ab")=\{3,5\}\)。那么,关于\(EndPos\)我们有如下几个结论:

  • 如果两个串的\(EndPos\)相同,那么其中一个一定是另一个的后缀。
  • 对于任意两个串\(S_1,S_2\)\(Length(S_1)\leqslant Length(S_2)\),那么\(EndPos(S_2)\subseteq EndPos(S_1)\)\(EndPos(S_1)\cap EndPos(S_2)=\emptyset\)
  • 对于所有\(EndPos(S)\)相等的\(S\),记这些\(S\)中长度最大的长度为\(MaxLen\),长度最小的为\(MinLen\),那么对于\(\forall i(MinLen \leqslant i\leqslant MaxLen), \exists S'(EndPos(S')=EndPos(S), Length(S')=i)\)
  • 不同的\(EndPos\)最多有\(O(n)\)类。

\(3\)点还是容易想象的,下面是对于第\(4\)点的证明:

不难发现\(\forall S, EndPos(S)\subseteq EndPos("")\)。对于某个特定的\(EndPos\)集合\(X\),取最长的\(S\)使得\(EndPos(S)=X\)。在\(X\)前面加两个不同的字符\(x,y\),那么\(EndPos(xS)\cap EndPos(yS)=\emptyset\)。而实际上还有

\[\left|\bigcup_{x\in Alpha}EndPos(xS)=EndPos(S)\right| \]

所以我们可以将这看成一个划分,划分关系构成一棵树,叫做Parent树。其中,一个节点的父亲的\(MaxLen\)是这个节点的\(MinLen-1\)。这棵树最多有\(2n-1\)个节点,即不同的\(EndPos\)的个数是\(O(n)\)的。

可以看图理解一下(图中黑色部分,黄色为满足某个特定的\(EndPos\)的最长\(S\)):

1

如果将这个数构建成一个完整的自动机,我们还需要定义:

  • \(Init\):根节点;
  • \(End\):图中带橙色叉的节点;
  • \(Trans\):图中红色的边;

同时我们还可以说SAM的边数是\(O(n)\)坑,待填

如何构建后缀自动机

先放两张图:

如果我们已经构建好了\("aaba"\)的SAM:

2

我们要构建\("aabab"\)的SAM:

3

上面这张图有点错误,从\(\{2\}\)指向\(\{3,5\}\)的边应该指向\(\{3\}\),从\(\{3\}\)应该有指向\(\{4\}\)的值为\(a\)的边。这点会在下面的构造过程中讲到。

我们发现,只需要更改\(EndPos\)中包含最后一个位置的点就可以了。如果我们记录了\(Last\),那需要处理的节点就是\(Last\)的祖先。不妨从叶子向根记做\(v_1,v_2,\dots\)。由于从叶子向根,\(|EndPos|\)是不断增大的。所以我们可以找到第一个\(Trans(v_i,x)\)不为空的点。将\(Trans(v_t,x)\)为空的指向新点\(newPoint\)即可,而对于另一些我们需要分类讨论。不妨记\(q=Trans(v_t,x)\)。如果强行加入\(x\),那么会使\(q\)\(MaxLen\)变小(当然,如果不会,那么构造就结束了)。加入\(x\)后,实际上\(q\)被分为了两个部分:

4

所以建一个新的点\(newq\)解决这个问题。\(newq\)继承了\(q\)除了\(MaxLen\)之外的所有信息。

同时,我们还要将原图中原来指向\(q\)的点指向\(nq\)。这样就结束了!

#include <bits/stdc++.h>
using namespace std;

const int Maxn = 1000010;
struct suffixAutomaton {
    int Link[ Maxn << 1 ], Len[ Maxn << 1 ], Child[ Maxn << 1 ][ 26 ];
    int Last, Used;
    inline void Init() {
        memset( Link, 0, sizeof( Link ) );
        memset( Len, 0, sizeof( Len ) );
        memset( Child, 0, sizeof( Child ) );
        Last = 1; Used = 1; 
        return; 
    }
    void Build( int Ch ) {
        int Now = ++Used, p;
        Len[ Now ] = Len[ Last ] + 1;
        for( p = Last; p && Child[ p ][ Ch ] == 0; p = Link[ p ] ) Child[ p ][ Ch ] = Now;
        Last = Now;
        if( !p ) { Link[ Now ] = 1; return; }
        int q = Child[ p ][ Ch ];
        if( Len[ p ] + 1 == Len[ q ] ) { Link[ Now ] = q; return; }
        int Clone = ++Used;
        Len[ Clone ] = Len[ p ] + 1; Link[ Clone ] = Link[ q ];
        for( int i = 0; i < 26; ++i ) Child[ Clone ][ i ] = Child[ q ][ i ];
        for( ; p && Child[ p ][ Ch ] == q; p = Link[ p ] ) Child[ p ][ Ch ] = Clone;
        Link[ q ] = Link[ Now ] = Clone;
        return;
    }
};
suffixAutomaton SuffixAutomaton;
char Ch[ Maxn ];
int Len;

int main() {
    scanf( "%s", Ch );
    Len = strlen( Ch );
    SuffixAutomaton.Init();
    for( int i = 0; i < Len; ++i )
        SuffixAutomaton.Build( Ch[ i ] - 'a' );
    return 0;
}

练习题

luogu模板

#include <bits/stdc++.h>
using namespace std;

const int Maxn = 1000010;
int Ref[ Maxn ], Arr[ Maxn << 1 ];
struct suffixAutomaton {
    int Link[ Maxn << 1 ], Len[ Maxn << 1 ], Child[ Maxn << 1 ][ 26 ];
	int Last, Used;
	//extra 
	int Size[ Maxn << 1 ];
	inline void Init() {
        memset( Link, 0, sizeof( Link ) );
        memset( Len, 0, sizeof( Len ) );
        memset( Child, 0, sizeof( Child ) );
        Last = 1; Used = 1; 
        return; 
    }
    void Build( int Ch ) {
        int Now = ++Used, p;
        Len[ Now ] = Len[ Last ] + 1;
		Size[ Now ] = 1;//不是copy的点才能做算大小
        for( p = Last; p && Child[ p ][ Ch ] == 0; p = Link[ p ] ) Child[ p ][ Ch ] = Now;
        Last = Now;
        if( !p ) { Link[ Now ] = 1; return; }
        int q = Child[ p ][ Ch ];
        if( Len[ p ] + 1 == Len[ q ] ) { Link[ Now ] = q; return; }
        int Clone = ++Used;
        Len[ Clone ] = Len[ p ] + 1; Link[ Clone ] = Link[ q ];
        for( int i = 0; i < 26; ++i ) Child[ Clone ][ i ] = Child[ q ][ i ];
        for( ; p && Child[ p ][ Ch ] == q; p = Link[ p ] ) Child[ p ][ Ch ] = Clone;
        Link[ q ] = Link[ Now ] = Clone;
        return;
    }
    inline void CollectSize( int MaxLen ) {//按照DAG的逆序求大小
    	memset( Ref, 0, sizeof( Ref ) );
    	memset( Arr, 0, sizeof( Arr ) );
    	for( int i = 1; i <= Used; ++i ) ++Ref[ Len[ i ] ];
    	for( int i = 1; i <= MaxLen; ++i ) Ref[ i ] += Ref[ i - 1 ];
    	for( int i = 1; i <= Used; ++i ) Arr[ Ref[ Len[ i ] ]-- ] = i;
    	for( int i = Used; i >= 1; --i ) Size[ Link[ Arr[ i ] ] ] += Size[ Arr[ i ] ];
    	return;
    }
};
suffixAutomaton SuffixAutomaton;
char Ch[ Maxn ];
int Len;

int main() {
    scanf( "%s", Ch );
    Len = strlen( Ch );
    SuffixAutomaton.Init();
    for( int i = 0; i < Len; ++i ) SuffixAutomaton.Build( Ch[ i ] - 'a' );
    SuffixAutomaton.CollectSize( Len );
    int Ans = 0;
    for( int i = 1; i <= SuffixAutomaton.Used; ++i )
    	if( SuffixAutomaton.Size[ i ] > 1 )
    		Ans = max( Ans, SuffixAutomaton.Len[ i ] * SuffixAutomaton.Size[ i ] );
    printf( "%d\n", Ans );
    return 0;
}

「TJOI2019」甲苯先生和大中锋的字符串

#include <bits/stdc++.h>
using namespace std;

const int Maxn = 100010;
int Ref[ Maxn ], Arr[ Maxn << 1 ];
struct suffixAutomaton {
    int Link[ Maxn << 1 ], Len[ Maxn << 1 ], Child[ Maxn << 1 ][ 26 ];
	int Last, Used;
	//extra 
	int Size[ Maxn << 1 ];
	inline void Init() {
        memset( Link, 0, sizeof( Link ) );
        memset( Len, 0, sizeof( Len ) );
        memset( Child, 0, sizeof( Child ) );
        memset( Size, 0, sizeof( Size ) );
        Last = 1; Used = 1; 
        return; 
    }
    void Build( int Ch ) {
        int Now = ++Used, p;
        Len[ Now ] = Len[ Last ] + 1;
		Size[ Now ] = 1;//不是copy的点才能做算大小
        for( p = Last; p && Child[ p ][ Ch ] == 0; p = Link[ p ] ) Child[ p ][ Ch ] = Now;
        Last = Now;
        if( !p ) { Link[ Now ] = 1; return; }
        int q = Child[ p ][ Ch ];
        if( Len[ p ] + 1 == Len[ q ] ) { Link[ Now ] = q; return; }
        int Clone = ++Used;
        Len[ Clone ] = Len[ p ] + 1; Link[ Clone ] = Link[ q ];
        for( int i = 0; i < 26; ++i ) Child[ Clone ][ i ] = Child[ q ][ i ];
        for( ; p && Child[ p ][ Ch ] == q; p = Link[ p ] ) Child[ p ][ Ch ] = Clone;
        Link[ q ] = Link[ Now ] = Clone;
        return;
    }
    inline void CollectSize( int MaxLen ) {//按照DAG的逆序求大小
    	memset( Ref, 0, sizeof( Ref ) );
    	memset( Arr, 0, sizeof( Arr ) );
    	for( int i = 1; i <= Used; ++i ) ++Ref[ Len[ i ] ];
    	for( int i = 1; i <= MaxLen; ++i ) Ref[ i ] += Ref[ i - 1 ];
    	for( int i = 1; i <= Used; ++i ) Arr[ Ref[ Len[ i ] ]-- ] = i;
    	for( int i = Used; i >= 1; --i ) Size[ Link[ Arr[ i ] ] ] += Size[ Arr[ i ] ];
    	return;
    }
};
suffixAutomaton SuffixAutomaton;
char Ch[ Maxn ];
int Len;
int Count[ Maxn ];

void Work() {
    scanf( "%s", Ch );
    Len = strlen( Ch );
    SuffixAutomaton.Init();
    for( int i = 0; i < Len; ++i ) SuffixAutomaton.Build( Ch[ i ] - 'a' );
    SuffixAutomaton.CollectSize( Len );
    int k; scanf( "%d", &k );
    int Max = 1, Ans = -1;
    memset( Count, 0, sizeof( Count ) );
    for( int i = 1; i <= SuffixAutomaton.Used; ++i )
    	if( SuffixAutomaton.Size[ i ] == k ) {
    		--Count[ SuffixAutomaton.Len[ i ] + 1 ];
    		++Count[ SuffixAutomaton.Len[ SuffixAutomaton.Link[ i ] ] + 1 ];
    	}
    for( int i = 1; i <= Len; ++i ) Count[ i ] += Count[ i - 1 ];
    for( int i = 0; i <= Len; ++i )
    	if( Count[ i ] >= Max ) {
    		Max = Count[ i ];
    		Ans = i;
    	}
    printf( "%d\n", Ans );
    return;
}

int main() {
	int TestCases; scanf( "%d", &TestCases );
	for( ; TestCases--; ) Work();
	return 0;
}
posted @ 2019-07-23 20:46  chy_2003  阅读(483)  评论(0编辑  收藏  举报