zoukankan      html  css  js  c++  java
  • 【知识点】后缀自动机

    后缀自动机:

    简介:

    通过巧妙的设计使得我们能用一个DAG和树的复合结构来在线性复杂度内存储一个串的$n^2$个子串的信息。

    定义:

    1.后缀自动机的结构类似于AC自动机,每个点表示一个endpos等价类(子串结束位置的集合,以下简称为状态),边同AC自动机中的边。即后缀自动机上从根到一个点有若干条路径,这些路径构成的子串在原串中的endpos集合都相同。

    2.parent树是一棵建立在后缀自动机节点上的树,建立的方法是类似于线段树的分割(见下文)。它满足任意点u代表的任意一个字符串是u子树中任意一个字符串的后缀。

    3.一些需要维护的信息:

    struct node{
        int ch[30]; //边(到某个点可能有很多条路径,它们同属于这个点的状态) 
        int dis; //到达该点的所有路径中最长的那一条的长度(该状态最长的串长) 
        int fa; //父亲(串长严格小于儿子;严格是儿子的后缀,但父子之间不一定没有边) 
    };

    性质:

    在提到性质之前我们先考虑一下这个endpos等价类的含义。

    以$aababa$这个串为例,它有6个可能的endpos,我们现在按长度依次考虑以i为endpos的每个子串$(s[1,i],s[2,i],cdots ,s[i,i])$所属的endpos等价类。

    初始都是空串,那么$endpos(空串)={1,2,3,4,5,6}$。

    然后考虑长度为1的子串,位置1,2,4,6是'a',位置3,5是'b'。那么$endpos('a')={1,2,4,6}$,$endpos('b')={3,5}$。

    然后考虑长度为2的子串,位置2是'aa',位置3,5是'ab',位置4,6是'ba'。那么$endpos('aa')={2}$,$endpos('ab')={3,5}$,$endpos('ba')={4,6}$。

    ……

    自己模拟一下这个过程,不难发现三点性质:

    1.随着长度的增加,endpos等价类从集合${1,2,cdots,n}$逐渐划分成若干个小集合。但不是完全划分,中间会因为某个前缀的长度不够而损失掉这个前缀。

    2.对于任意两个子串,它们的endpos等价类要么互为子集,要么不相交。显然前者是互为后缀的情况,后者是不互为后缀的情况。

    3.同一个endpos等价类包含的子串实际上是某个极长子串的所有后缀。因为我们是在逐渐往每个endpos前面塞字符,那么某个等价类里的所有endpos前面塞完一个字符后,它们代表的字符串可能从相同变得不同。此时该等价类被划分,它的极长子串就是塞之前的子串。

    构造:

    使用增量构造法,就是假设前n-1个字符已经构造好了,考虑插入第n个字符c会怎么样,也就是如何处理插入后新产生的子串(新串的n个后缀)。

    先建一个代表${n}$等价类的点np,然后从上一次插入的点last(即代表${n-1}$等价类,$s[1,n-1]$的点)开始跳fa,每跳到一个点p就把它往np连一条字符边c。直到跳到0(不是根)或者当前点p已经有一条字符c出边。

    如果跳到0了说明原串里根本就没出现c这个字符,于是直接记$x[np].fa=rt$即可。

    否则,设$q=x[p].to[c]$,分q是或不是新串的后缀两种情况讨论。

    当且仅当$x[p].dis+1=x[q].dis$时,q是新串的后缀(q里的最长串就是p里的最长串+c)。此时新串长度$>x[p].dis+1$的后缀都连过边c了,而长度$leq x[p].dis+1$的后缀已经在原串出现过了(就是q代表的所有子串),所以整个系统合法,只需要令$x[np].fa=q$。

    否则,q不是新串的后缀(q里的最长串的后缀是p里的最长串+c)。此时我们把q拆成两个点nq和q,前者是新串的后缀(即长度$leq x[p].dis+1$的部分),后者不是。拆完之后把p所有的祖先原来连到q的c边都改到nq(因为p的祖先+c是新串的后缀),然后记$x[nq].fa=x[q].fa,x[q].fa=x[np].fa=nq$即可。此时整个系统合法。

    关于为什么$x[p].dis+1 eq x[q].dis$时q不是新串的后缀:因为如果是,那么q中最长串去掉c后必然是一个长度$>x[p].dis$的原串后缀,且它代表的节点有c出边。那么这个节点应该先于p被遍历到并且停下,而这并没有发生,所以q一定不是新串的后缀。

    代码:

    #include<bits/stdc++.h>
    #define maxn 1000005
    #define maxm 500005
    #define inf 0x7fffffff
    #define ll long long
    #define debug(x) cerr<<#x<<": "<<x<<endl
    #define fgx cerr<<"--------------"<<endl
    #define dgx cerr<<"=============="<<endl
    
    using namespace std;
    int hd[maxn<<1],nxt[maxn<<1],to[maxn<<1],ans,cnt;
    char str[maxn<<1];
    
    inline int read(){
        int x=0,f=1; char c=getchar();
        for(;!isdigit(c);c=getchar()) if(c=='-') f=-1;
        for(;isdigit(c);c=getchar()) x=x*10+c-'0';
        return x*f;
    }
    inline void addedge(int u,int v){to[++cnt]=v,nxt[cnt]=hd[u],hd[u]=cnt;}
    
    struct SAM{
        struct node{int to[30],fa,siz,dis;}x[maxn<<1];
        int tot=1,lst=1,rt=1;
        inline void extend(int pos){
            int val=str[pos]-'a',np=++tot,p=lst; 
            lst=np,x[np].siz=1,x[np].dis=pos;
            for(;p&&!x[p].to[val];p=x[p].fa) x[p].to[val]=np;
            if(!p) x[np].fa=rt;
            else{
                int q=x[p].to[val];
                if(x[q].dis==x[p].dis+1) x[np].fa=q;
                else{
                    int nq=++tot; x[nq].dis=x[p].dis+1;
                    memcpy(x[nq].to,x[q].to,sizeof(x[q].to));
                    x[nq].fa=x[q].fa,x[np].fa=x[q].fa=nq;
                    for(;x[p].to[val]==q;p=x[p].fa) x[p].to[val]=nq;
                }
            }
        }
        inline void dfs(int u){
            for(int i=hd[u];i;i=nxt[i]) dfs(to[i]),x[u].siz+=x[to[i]].siz;
            if(x[u].siz!=1) ans=max(ans,x[u].siz*x[u].dis);
        }
    }at;
    
    int main(){
        scanf("%s",str+1);
        int n=strlen(str+1);
        for(int i=1;i<=n;i++) at.extend(i);
        for(int i=2;i<=at.tot;i++) addedge(at.x[i].fa,i);
        at.dfs(1),printf("%d
    ",ans);
        return 0;
    }
    后缀自动机

    广义后缀自动机:

    没啥区别,每次插入一个串的时候直接把last设为1就行了,相当于从头开始插入。

    #include<bits/stdc++.h>
    #define maxn 1000005
    #define maxm 500005
    #define inf 0x7fffffff
    #define ll long long
    #define rint register int
    #define debug(x) cerr<<#x<<": "<<x<<endl
    #define fgx cerr<<"--------------"<<endl
    #define dgx cerr<<"=============="<<endl
    
    using namespace std;
    char str[maxn];
    
    inline int read(){
        int x=0,f=1; char c=getchar();
        for(;!isdigit(c);c=getchar()) if(c=='-') f=-1;
        for(;isdigit(c);c=getchar()) x=x*10+c-'0';
        return x*f;
    }
    
    struct RAMauto{
        int to[maxn<<1][30],dis[maxn<<1],fa[maxn<<1],vis[maxn<<1],tot;
        inline void insert(int n){
            int las=1;
            for(int i=1;i<=n;i++){
                int ch=str[i]-'a',p=las,now=++tot; 
                dis[now]=i,las=now;
                while(p && !to[p][ch]) to[p][ch]=now,p=fa[p];
                if(!p) fa[now]=1;
                else{
                    int q=to[p][ch];
                    if(dis[q]==dis[p]+1) fa[now]=q;
                    else{
                        int nq=++tot; dis[nq]=dis[p]+1;
                        memcpy(to[nq],to[q],sizeof(to[q]));
                        fa[nq]=fa[q],fa[q]=fa[now]=nq;
                        while(p && to[p][ch]==q) to[p][ch]=nq,p=fa[p];
                    }
                }
            }
        }
        inline ll dfs(int u){
            if(vis[u]) return 0; vis[u]=1; 
            ll res=(ll)dis[u]-(ll)dis[fa[u]];
            for(int i=0;i<26;i++)
                if(to[u][i]) res+=dfs(to[u][i]);
            return res;
        }
    }RAM;
    
    int main(){
        int T=read(); RAM.tot=1;
        while(T--) scanf("%s",str+1),RAM.insert(strlen(str+1));
        printf("%lld
    ",RAM.dfs(1));
        return 0;
    }
    广义后缀自动机
  • 相关阅读:
    declare set声明注意
    Winform 的dadagridview控件的修改操作
    VS2010,VS2008,VS2005;工程之间的转换
    C#程序跨平台?
    上网黑色护眼,设置浏览器黑色风格
    AutoCompleteSource从文件里读取自动填充内容
    两个checkbox的控件控制操作只能选其一
    《博客园精华集CLR/C#分册》第三轮筛选结果 转载
    TransactSQL 示例 查询某个数据库内的所有表的记录行数及其总和
    EF 4.1中内部经常提交的 exec sp_reset_connection 的用途原来是为了重用池中的连接
  • 原文地址:https://www.cnblogs.com/YSFAC/p/12289078.html
Copyright © 2011-2022 走看看