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;
    }
    
  • 相关阅读:
    如何只通过Sandboxed Solution启动一个定时执行的操作
    创建与SharePoint 2010风格一致的下拉菜单 (续) 整合Feature Custom Action框架
    创建与SharePoint 2010风格一致的下拉菜单
    《SharePoint 2010 应用程序开发指南》第二章预览
    SharePoint 2013 App 开发 (1) 什么是SharePoint App?
    使用Jscex增强SharePoint 2010 JavaScript Client Object Model (JSOM)
    搜索范围的管理
    SharePoint 2010 服务应用程序(Service Application)架构(1)
    SharePoint 2010 服务应用程序(Service Application)架构(2)
    SharePoint 2013 App 开发 (2) 建立开发环境
  • 原文地址:https://www.cnblogs.com/chy-2003/p/11234358.html
Copyright © 2011-2022 走看看