后缀自动机的性质应用

原创 2017年03月29日 09:50:41

之前自己整理过后缀自动机的构造,现在再来整理一波性质。

最长公共子串

这个问题非常常见,大概可以根据难易程度分成两种。

两个串的最长公共子串

这个问题可以用DP来解决,也可以用后缀数组。
DP的效率比较低,后缀数组的话将两个串用分隔符连接起来求出height数组,取sa[i],sa[i-1]分别属于两个串的height的最大值即可。
那么利用后缀自动机该怎么做呢?我们将A串建立后缀自动机,然后用B串在后缀自动机上进行匹配,因为后缀自动机中包含A串的所有子串,并且root到后缀自动机中的任意节点形成的路径都是A的合法子串,所有B串能匹配到的点到根的路径都是AB的最长公共子串。那么我们只要找到所有能匹配的节点到根的最远距离即可。如果匹配到一个点后匹配不上了怎么办呢?后缀自动机中有一个很重要的指针——parent指针,每个点的parent指针指向的是上一个可以接受后缀的节点,即如果当前节点可以接某个后缀那么parent指针指向的节点也一定可以接,可以将parent指针指向的位置表示的状态看成该状态的一个后缀。那么匹配不上我们就不停的跳parent链,直到匹配上或者回到根节点为止。跳到一个节点后当前匹配的长度就变成了l[i](i指可以匹配上的节点)

#include<iostream>  
#include<cstdio>  
#include<algorithm>  
#include<cstring>  
#include<cmath>  
#define N 200003  
using namespace std;  
int n,m,ch[N][30],fa[N],a[N],l[N],np,nq,p,q,last,cnt,root;  
char s[N],s1[N];  
void extend(int x)  
{  
    int c=a[x];  
    p=last; np=++cnt; last=np;  
    l[np]=x;  
    for (;p&&!ch[p][c];p=fa[p]) ch[p][c]=np;  
    if (!p) fa[np]=root;  
    else {  
        int q=ch[p][c];  
        if (l[p]+1==l[q]) fa[np]=q;  
        else {  
            int nq=++cnt; l[nq]=l[p]+1;  
            memcpy(ch[nq],ch[q],sizeof ch[nq]);  
            fa[nq]=fa[q];  
            fa[np]=fa[q]=nq;  
            for (;ch[p][c]==q;p=fa[p]) ch[p][c]=nq;   
        }  
    }  
}  
int solve()  
{  
    int tmp=0,ans=0;   
    for (int i=1;i<=m;i++) {  
        int c=s1[i]-'a';  
        if (ch[p][c]) p=ch[p][c],tmp++;  
        else {  
            while (p&&!ch[p][c]) p=fa[p];  
            if (!p) p=1,tmp=0;  
            else {  
                tmp=l[p]+1;  
                p=ch[p][c];  
            }  
        }  
        ans=max(ans,tmp);  
    }  
    return ans;  
}  
int main()  
{  
    freopen("a.in","r",stdin);  
    scanf("%s",s+1);  
    n=strlen(s+1); last=root=++cnt;  
    for (int i=1;i<=n;i++) a[i]=s[i]-'a';  
    for (int i=1;i<=n;i++) extend(i);  
    scanf("%s",s1+1);  
    m=strlen(s1+1);  
    printf("%d",solve());  
}  

题目链接:code vs 3160     题解戳这里

多个串的最长公共子串

这个问题的话用后缀数组应该还是可以做的,应该是利用二分答案,稍微麻烦一点。
那后缀自动机呢?还是对第一个串建立后缀自动机,不过这次匹配的时候是多个串进行匹配。每次匹配的时候对于后缀自动机中的每个节点维护一个值h,表示的是到达该节点所能匹配上的最大长度(在构造的时候说过,每个节点都可能是多个节点的儿子,所以从根到该点的路径长度可能是不同的)。光匹配还不够,我们需要按照拓扑序倒序,用每个节点取更新他的parent节点,因为如果匹配到一个状态,那么实际上他parent链上的所有状态都匹配上了,h[fa[i]]=l[i](l表示的是从根节点到该节点的最大距离)。然后对于每个串匹配后得到的每个位置的h数组取min得到数组g,g的最大值极为答案。
PS:所谓的拓扑倒序就是按照L从大到小。

#include<iostream>    
#include<cstdio>    
#include<cstring>    
#include<algorithm>    
#include<cmath>    
#define N 200005    
using namespace std;    
int ch[N][30],fa[N],l[N],cl[N],mn[N],ans,a[N];    
int n,m,last,root,p,q,np,nq,cnt,v[N],pos[N];    
char s[N],s1[N];     
void extend(int x)    
{    
    int c=s[x]-'a';    
    p=last; np=++cnt; last=np;    
    l[np]=x;    
    for (;p&&!ch[p][c];p=fa[p]) ch[p][c]=np;    
    if (!p) fa[np]=root;    
    else {    
        q=ch[p][c];    
        if (l[p]+1==l[q]) fa[np]=q;    
        else {    
            nq=++cnt; l[nq]=l[p]+1;    
            memcpy(ch[nq],ch[q],sizeof ch[nq]);    
            fa[nq]=fa[q];    
            fa[q]=fa[np]=nq;    
            for (;ch[p][c]==q;p=fa[p]) ch[p][c]=nq;    
        }    
    }    
}    
void solve()    
{    
    int tmp=0;  p=root;  
    memset(cl,0,sizeof(cl));    
    for (int i=1;i<=n;i++){    
        int c=s[i]-'a';    
        if (ch[p][c]) p=ch[p][c],tmp++;    
        else {    
            while (p&&!ch[p][c]) p=fa[p];    
            if (!p) p=1,tmp=0;    
            else tmp=l[p]+1,p=ch[p][c];    
        }    
        cl[p]=max(cl[p],tmp);    
    }    
    for (int i=cnt;i>=1;i--){    
      int t=pos[i];    
      mn[t]=min(mn[t],cl[t]);    
      if (cl[t]&&fa[t]) cl[fa[t]]=l[fa[t]];    
    }    
}    
int main()    
{    
    //freopen("a.in","r",stdin);  
    int t=0;    
    memset(mn,127,sizeof(mn));    
    while (scanf("%s",s+1)!=EOF) {    
        t++; n=strlen(s+1);    
        if (t==1) {    
            root=last=++cnt;    
            for (int i=1;i<=n;i++) extend(i);    
            for (int i=1;i<=cnt;i++) v[l[i]]++;    
            for (int i=1;i<=n;i++) v[i]+=v[i-1];    
            for (int i=1;i<=cnt;i++) pos[v[l[i]]--]=i;    
        }    
        else solve();    
    }    
    for (int i=1;i<=cnt;i++) ans=max(ans,mn[i]);    
    printf("%d\n",ans);    
}     

题目链接:spoj 1812     题解戳这里
bzoj 2946: [Poi2000]公共串     题解戳这里

第K小子串问题

这类问题多与拓扑序有一定的关系,我们按照拓扑倒序用每个点取更新fa的取值,就可以得到每个位置后面有多少个子串。然后在后缀自动机上进行dfs,每次从字典序小的开始计算,如果当前子树(说是他的儿子或者一大坨后继更准确,因为不是树的形态)的size>=k就说明第k小的子串的结尾在该子树中,否则k-size,然后从下个继续。有点主席树查询区间k大的意思。
spoj 7258     题解戳这里
bzoj 3998: [TJOI2015]弦论    题解戳这里
bzoj 2882: 工艺    题解戳这里

重复出现的子串问题

这类问题一般都与right集合有关系,所谓right集合就是某个状态或者说是子串str在s中每次出现位置的右端点组成的集合,|right|常用来表示right集合的大小。
那么right的大小怎么求呢?right(i)是right(fa[i])的子集,所以我们按照parent树中深度从大到小,依次将每个状态的right集合并入他fa状态的right集合,初始的时候只有主链上的|right|=1
按照这种方式我们不仅可以求出right集合的大小,还可以求出某个子串在字符串中出现的最靠左最靠右的位置等等,再次不在赘述。
需要注意的是每个状态str表示的串的长度是区间(len(fa),len(str)]每个状态str表示的串在原串中的出现次数及出现的右端点相同。也就是每个状态只有一个|right|的意思。
这类问题静态的比较好搞,如果是动态的可能需要借助数据结构进行维护,比如下面的bzoj2555就需要用到LCT动态维护parent树的形态,并维护节点的信息。
poj 1743 Musical Theme    题解戳这里
spoj 8222 NSUBSTR-Substrings    题解戳这里
bzoj 2555: SubString    题解戳这里

广义后缀自动机

近些年新兴的算法。一般有两种形式,一种是对trie建立广义的后缀自动机,另一种是对多个独立的串建立广义的后缀自动机。实现的方式差不多。
对于trie树:我们在只有一个串的时候p就是直接指向前一个字符的位置,因为我们现在是树结构,所以p指向的应该是该点在trie树中父节点的位置,剩下的建立过程与普通的后缀自动机相同。
对于多个串:每次加完一个串就把p回到root。然后自然加入即可。
bzoj 3926: [Zjoi2015]诸神眷顾的幻想乡    题解戳这里
bzoj 2780: [Spoj]8093 Sevenk Love Oimaster     题解戳这里
bzoj 3277: 串    题解戳这里
bzoj bzoj 3473: 字符串    题解戳这里

后缀自动机与DP的完美结合

这类题的重点一般不在后缀自动机,一般后缀自动机只是做预处理用的,关键是DP思路。
bzoj 4180: 字符串计数    题解戳这里
bzoj 4032: [HEOI2015]最短不公共子串    题解戳这里
bzoj 2806: [Ctsc2012]Cheat    题解戳这里
bzoj 3238: [Ahoi2013]差异    题解戳这里

right集合与parent树

right集合与parent树是后缀自动机最常用也是最好用的两个东西。
right集合一般用来处理计数问题。两者相辅相成不可分离。
有一个比较有用的性质:两个串的最长公共后缀,位于这两个串对应状态在parent树的最近公共祖先上。
bzoj 4516: [Sdoi2016]生成魔咒    题解戳这里
bzoj 4566: [Haoi2016]找相同字符    题解戳这里
bzoj 1396: 识别子串    题解戳这里

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/clover_hxy/article/details/68059043

后缀自动机详解

后缀自动机 后缀自动机(单词的有向无环图)——是一种强有力的数据结构,让你能够解决许多字符串问题。 例如,使用后缀自动机可以在某一字符串中搜索另一字符串的所有出现位置,或者计算不同子串的个数——这都...
  • qq_35649707
  • qq_35649707
  • 2017-03-26 11:34:54
  • 10614

后缀自动机学习小记

简介后缀三姐妹:后缀数组,后缀自动机,后缀树。 后缀自动机:Suffix Automation,也叫SAM。 创立算法的思路来源:能不能构出一个自动机(本质就是一个有向图),能识别一个串的所有后缀...
  • doyouseeman
  • doyouseeman
  • 2016-08-18 22:11:24
  • 1304

后缀自动机(SAM)学习笔记

构图及原理定义算法后缀自动机(SAM)就是一个要实现能存下一个串中所有子串的算法,按一般来说应当有O(N2)O(N^2)个状态,而SAM却可以用O(N)个状态来表示所有子串,因为它把很多个本质相似的子...
  • YxuanwKeith
  • YxuanwKeith
  • 2016-03-30 20:31:38
  • 4564

后缀自动机总结

后缀自动机总结 后缀自动机的构造和相关性质及复杂度证明可以看陈老师的ppt 时间复杂度据说可以用均摊分析证明是O(n)的 一开始看直接看陈老师的ppt确实有点难以理解,但是陈老师的ppt确实...
  • thy_asdf
  • thy_asdf
  • 2016-06-02 21:53:41
  • 6748

hihocoder 127 后缀自动机一·基本概念

自己想了老半天,写了三百多行,没实现成功,忘记使用STL库了。。。。。。。。。。。。。。。 题目链接:http://hihocoder.com/problemset/problem/1441 ...
  • liyuanshuo_nuc
  • liyuanshuo_nuc
  • 2016-12-10 22:04:55
  • 907

后缀自动机初探

定义给定字符串S, S 的后缀自动机(SAM)是一个能够识别S 的所有后缀的自动机一些记号 trans(s,x):状态s走x转移到达的状态 reg(s):状态s能接受的状态,即trans(s,str)...
  • D042412
  • D042412
  • 2016-05-16 22:15:46
  • 897

广义后缀自动机

广义后缀自动机: 传统后缀自动机是解决单个主串的匹配问题,广义后缀自动机可以用来解决多个主串的匹配问题。...
  • wangzhen_yu
  • wangzhen_yu
  • 2015-05-04 15:02:33
  • 3544

ac自动机最详细的讲解,让你一次学会ac自动机。

在没学ac自动机之前,觉得ac自动机是个很神奇,很高深,很难的算法,学完之后发现,ac自动机确实很神奇,很高深,但是却并不难。 我说ac自动机很神奇,在于这个算法中失配指针的妙处(好比kmp算法中的...
  • creatorx
  • creatorx
  • 2017-05-02 19:51:08
  • 22872

后缀排序 后缀自动机的应用

同步个人博客 http://sxysxy.org/blogs/23 到csdn)给一字符串的所有后缀排个序输出,字符串长度 ...
  • u013632138
  • u013632138
  • 2016-08-31 13:57:34
  • 1042

后缀自动机初步

想学这个算法很久了 省选结束后终于下定决心好好学学! 先推荐两篇文章 http://hi.baidu.com/myidea/item/142c5cd45901a51820e25039?qq-pf-to...
  • Zeyu_King
  • Zeyu_King
  • 2015-04-23 20:01:13
  • 1318
收藏助手
不良信息举报
您举报文章:后缀自动机的性质应用
举报原因:
原因补充:

(最多只允许输入30个字)