后缀自动机的性质应用

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

最长公共子串

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

两个串的最长公共子串

这个问题可以用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: 识别子串     题解戳这里

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值