zoukankan      html  css  js  c++  java
  • 后缀自动机详解

    前言

    (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(Rin {Statecup {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),(Sin 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 ileqslant 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|igcup_{xin Alpha}EndPos(xS)=EndPos(S) ight| ]

    所以我们可以将这看成一个划分,划分关系构成一棵树,叫做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
    ", 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
    ", Ans );
        return;
    }
    
    int main() {
    	int TestCases; scanf( "%d", &TestCases );
    	for( ; TestCases--; ) Work();
    	return 0;
    }
    
  • 相关阅读:
    虚方法 C++快速入门23
    抽象方法 C++快速入门24
    网址探测器 零基础入门学习Delphi41
    网址探测器 零基础入门学习Delphi41
    抽象方法 C++快速入门24
    虚方法 C++快速入门23
    运算符重载 C++快速入门25
    静态属性和静态方法2 C++快速入门22
    linux系统中的文件访问控制列表ACL
    linux系统中的文件类型
  • 原文地址:https://www.cnblogs.com/chy-2003/p/11234358.html
Copyright © 2011-2022 走看看