后缀自动机专题

本文详细介绍了后缀自动机在处理字符串问题中的多种应用,包括存在性查询、不同的子串个数、不同子串的总长、字典序第k小子串、最小循环移位、出现次数查询、首次出现位置查询、所有出现位置查询、查询不在文本中出现的最短字符串、求两个字符串的最长公共子串以及多个字符串的最长公共子串。通过预处理和动态规划,利用后缀自动机可以在线性时间内解决这些问题。
摘要由CSDN通过智能技术生成

https://www.cnblogs.com/--560/p/5457023.html

SPOJ Longest Common Substring

求两串的最长公共子串

#include<bits/stdc++.h>
using namespace std;
const int MAX=2e6;
const int INF=0x3f3f3f3f;
struct Tire
{
    static const int MAXN=26;
    int nxt[MAX][MAXN],f[MAX],L[MAX],last,tot;
    void init()
    {
        last=tot=0;
        memset(nxt[0],-1,sizeof(nxt[0]));
        f[0]=-1;L[0]=0;
    }
    void add(int x)
    {
        int p=last,np=++tot;
        L[np]=L[p]+1;
        memset(nxt[np],-1,sizeof(nxt[np]));
        while(~p&&nxt[p][x]==-1) nxt[p][x]=np,p=f[p];
        if(p==-1) f[np]=0;
        else
        {
            int q=nxt[p][x];
            if(L[q]!=L[p]+1)
            {
                int nq=++tot;
                L[nq]=L[p]+1;
                memcpy(nxt[nq],nxt[q],sizeof(nxt[q]));
                f[nq]=f[q];
                f[q]=f[np]=nq;
                while(~p&&nxt[p][x]==q) nxt[p][x]=nq,p=f[p];
            }
            else f[np]=q;
        }
        last=np;
    }
    int find(char *s)
    {
        int len=strlen(s);
        int res=0,tmp=0,u=0;
        for(int i=0;i<len;++i)
        {
            int x=s[i]-'a';
            if(~nxt[u][x]) ++tmp,u=nxt[u][x];
            else
            {
                while(~u&&nxt[u][x]==-1) u=f[u];
                if(~u) tmp=L[u]+1,u=nxt[u][x];
                else tmp=0,u=0;
            }
            res=max(res,tmp);
        }
        return res;
    }
}SAM;
char s[MAX];
int main()
{
    while(~scanf("%s",s))
    {
        SAM.init();
        int len=strlen(s);
        for(int i=0;i<len;++i) SAM.add(s[i]-'a');
        scanf("%s",s);
        printf("%d\n",SAM.find(s));
    }
    return 0;
}

SPOJ Longest Common Substring II

多串最长公共公共子串

#include<bits/stdc++.h>
using namespace std;
const int MAX=1e6+5;
const int INF=0x3f3f3f3f;
struct Tire
{
    static const int MAXN=26;
    int nxt[MAX][MAXN],f[MAX],L[MAX],last,tot;
    int Min[MAX],Max[MAX];
    void init()
    {
        last=tot=0;
        memset(nxt[0],-1,sizeof(nxt[0]));
        f[0]=-1;L[0]=0;
        Min[0]=Max[0]=0;
    }
    void add(int x)
    {
        int p=last,np=++tot;
        L[np]=L[p]+1;
        memset(nxt[np],-1,sizeof(nxt[np]));
        Min[np]=L[np];
        while(~p&&nxt[p][x]==-1) nxt[p][x]=np,p=f[p];
        if(p==-1) f[np]=0;
        else
        {
            int q=nxt[p][x];
            if(L[q]!=L[p]+1)
            {
                int nq=++tot;
                L[nq]=L[p]+1;
                memcpy(nxt[nq],nxt[q],sizeof(nxt[q]));
                Min[nq]=L[nq];
                f[nq]=f[q];
                f[q]=f[np]=nq;
                while(~p&&nxt[p][x]==q) nxt[p][x]=nq,p=f[p];
            }
            else f[np]=q;
        }
        last=np;
    }
    void find(char *s)
    {
        int len=strlen(s);
        int u=0,tmp=0;
        for(int i=0;i<=tot;++i) Max[i]=0;
        for(int i=0;i<len;++i)
        {
            int x=s[i]-'a';
            if(~nxt[u][x]) ++tmp,u=nxt[u][x];
            else
            {
                while(~u&&nxt[u][x]==-1) u=f[u];
                if(~u) tmp=L[u]+1,u=nxt[u][x];
                else tmp=0,u=0;
            }
            Max[u]=max(Max[u],tmp);
        }
        for(int i=tot;i>=1;--i) Max[f[i]]=max(Max[f[i]],Max[i]);
        for(int i=0;i<=tot;++i) Min[i]=min(Min[i],Max[i]);
    }
    int cal()
    {
        int res=0;
        for(int i=0;i<=tot;++i) res=max(res,Min[i]);
        return res;
    }
}SAM;
char s[MAX];
int main()
{
    SAM.init();
    scanf("%s",s);
    int len=strlen(s);
    for(int i=0;i<len;++i) SAM.add(s[i]-'a');
    while(~scanf("%s",s)) SAM.find(s);
    printf("%d\n",SAM.cal());
    return 0;
}

SPOJ Substrings

求一个串长度为1~len的子串的最大个数

#include<bits/stdc++.h>
using namespace std;
const int MAX=1e6+5;
const int INF=0x3f3f3f3f;
int ans[MAX];
struct Tire
{
    static const int MAXN=26;
    int nxt[MAX][MAXN],f[MAX],L[MAX],rt[MAX],in[MAX],last,tot;
    void init()
    {
        last=tot=0;
        memset(nxt[0],-1,sizeof(nxt[0]));
        f[0]=-1;L[0]=0;
    }
    void add(int x)
    {
        int p=last,np=++tot;
        L[np]=L[p]+1;
        memset(nxt[np],-1,sizeof(nxt[np]));
        rt[np]=1;
        while(~p&&nxt[p][x]==-1) nxt[p][x]=np,p=f[p];
        if(p==-1) f[np]=0;
        else
        {
            int q=nxt[p][x];
            if(L[q]!=L[p]+1)
            {
                int nq=++tot;
                memcpy(nxt[nq],nxt[q],sizeof(nxt[q]));
                L[nq]=L[p]+1;
                f[nq]=f[q];
                f[q]=f[np]=nq;
                while(~p&&nxt[p][x]==q) nxt[p][x]=nq,p=f[p];
            }
            else f[np]=q;
        }
        last=np;
    }
    void cal()//topsort
    {
        memset(in,0,sizeof(in));
        for(int i=1;i<=tot;++i) ++in[f[i]];
        queue<int >q;
        for(int i=1;i<=tot;++i) if(!in[i]) q.push(i);
        while(!q.empty())
        {
            int u=q.front();q.pop();
            if(f[u]==-1) continue;
            rt[f[u]]+=rt[u];
            if(--in[f[u]]==0) q.push(f[u]);
        }
        memset(ans,0,sizeof(ans));
        for(int i=1;i<=tot;++i) ans[L[i]]=max(ans[L[i]],rt[i]);
    }
}SAM;
char s[MAX];
int main()
{
    while(~scanf("%s",s))
    {
        SAM.init();
        int len=strlen(s);
        for(int i=0;i<len;++i) SAM.add(s[i]-'a');
        SAM.cal();
        for(int i=len-1;i>=1;--i) ans[i]=max(ans[i],ans[i+1]);
        for(int i=1;i<=len;++i) printf("%d\n",ans[i]);
    }
    return 0;
}
 

后缀自动机一·基本概念

   
 

后缀自动机二·重复旋律5

L[i]-L[f[i]]  
 

后缀自动机三·重复旋律6

   
 

后缀自动机四·重复旋律7

   
 

后缀自动机五·重复旋律8

   
 

后缀自动机六·重复旋律9

   

在解决问题中的应用

下面看在后缀自动机的帮助下我们能做什么。

简便起见,我们假设字母表的大小k为常数。

存在性查询

问题.给定文本T,询问格式如下:给定字符串P,问P是否是T的子串。 

复杂度要求.预处理O(length(T)),每次询问O(P)。

算法.我们对文本T用O(length(T))建立后缀自动机。

现在回答单次询问。假设状态——变量v,最初是初始状态T_0.我们沿字符串P给出的路径走,因此从当前状态经转移来到新的状态v

。如果在某时刻,当前状态没有要求字符的转移,那么答案就是"no"。如果我们处理了整个字符串P,答案就是"yes"。

显然这一算法将在时间O(length(P))内运行完毕。并且,该算法实际上找出了P在文本中出现过的最长前缀——如果模式串使得这些前缀

都很短,算法将比处理全部模式串要快得多。

不同的子串个数

问题.给定字符串S,问它有多少不同的子串。

 复杂度要求.O(length(S))。

算法.我们将字符串S建立后缀自动机。

 在后缀自动机中,S的任意子串都对应自动机中的一条路径。答案就是从初始节点t_0开始,自动机中不同的路径条数。

 已知后缀自动机是一张有向无环图,我们可以考虑用动态规划计算不同的路径数量。

  也就是,令d[v]为从状态v开始的不同路径条数(包括长度为零的路径),则有转移:



 

即d[v]是v所有后继节点的d值之和加上1.

最终答案就是d[t_0]-1(减一以忽略空串)。

不同子串的总长

问题.给定字符串S,求其所有不同子串的总长度。

复杂度要求.O(length(S)).

算法.这一问题的答案和上一题类似,但现在我们必须考虑两个状态:不同子串的个数d[v]和它们的总长ans[v].

  上一题已描述了d[v]的计算方法,而ans[v]的计算方法如下:

即取所有后继节点w的ans值,并将它和d[w]相加。因为这是每个字符串的首字母。

字典序第k小子串

问题.给定字符串S,一系列询问——给出整数K_i,计算S的所有子串排序后的第K_i个。

复杂度要求.单次询问O(length(ans)*Alphabet),其中ans是该询问的答案,Alphabet是字母表大小。

 算法.这一问题的基础思路和上两题类似。字典序第k小子串——自动机中字典序第k小的路径。因此,考虑从每个状态出

发的不同路径数,我们将得以轻松地确定第k小路径,从初始状态开始逐位确定答案。

最小循环移位

问题.给定字符串S,找到和它循环同构的字典序最小字符串。

 复杂度要求.O(length(S)).

算法.我们将字符串S+S建立后缀自动机。该自动机将包含和S循环同构的所有字符串。

 从而,问题就简化成了在自动机中找出字典序最小的,长度为length(S)的路径,这很简单:从初始状态开始,每一步都贪心地走

,经过最小的转移。

出现次数查询

问题.给定文本T,询问格式如下:给定字符串P,希望找出P作为子串在文本T中出现了多少次(出现区间可以相交)。

 复杂度要求.预处理O(length(T)),单次询问O(length(P)).

算法.我们将文本T建立后缀自动机。

 然后我们需要进行预处理:对自动机中的每个状态v都计算cnt[v],等于其endpos(v)集合的大小。事实上,所有在T中对应同一状态的

字符串都在T中出现了相同次数,该次数等于endpos中的位置数。

 不过,我们无法对所有状态明确记录endpos集合,所以我们只计算其大小cnt.

 为了实现这一点,如下处理。对每个状态,如果它不是由“拷贝”而来,最初就赋值cnt=1.然后我们按长度len降序遍历所有序列,并将

当前的cnt[v]加给后缀链接:

 cnt[link(v)]+=cnt[v].

  你可能会说我们并没有对每个状态计算出了正确的cnt值。

 为什么这是对的?不经“拷贝”而来的状态恰好有length(S)个,而且其中的第i个是我们添加第i个字符时得到的。因此,最初这些状态的cnt=1,

其他状态的cnt=0.

 然后我们对每个状态v执行如下操作:cnt[link(v)]+=cnt[v].其意义在于,如果某字符串对应状态v,曾在cnt[v]中出现过,那么它的所有后缀都

同样在其中出现。

 这样,我们就掌握了如何对自动机中所有状态计算cnt值的方法。

 在此之后,询问的答案就变得平凡——只需要返回cnt[t],其中t是模式串P所对应的状态。

首次出现位置查询

问题.给定文本T,询问格式如下:给定字符串P,求P在文本中第一次出现的位置。

 复杂度要求.预处理O(length(T)),单次询问O(length(P)).

算法.对文本T建立后缀自动机。

 为了解决这一问题,我们需要预处理firstpos,找到自动机中所有状态的出现位置,即,对每个状态v我们希望找到一个位置firstpos[v],代表

其第一次出现的位置。换句话说,我们希望预先找出每个endpos(v)中的最小元素(我们无法明确记录整个endpos集合)。

  维护这些firstpos的最简单方法是在构建自动机时一并计算,当我们创建新的状态cur时,一旦进入函数sa_extend(),就确定该值:

 firstpos(cur)=len(cur)-1(如果我们的下标从0开始)。

当拷贝节点q时,令:

 firstpos(clone)=firstpos(q),(因为只有一个别的可能值——firstpos(cur),显然更大)。

 这样就得到了查询的答案——firstpos(t)-length(P)+1,其中t是模式串P对应的状态。

所有出现位置查询

问题.给定文本T,询问格式如下:给定字符串P,要求给出P在T中的所有出现位置(出现区间可以相交)。

复杂度要求.预处理O(length(T))。单次询问O(length(P)+answer(P)),其中answer(P)是答案集合的大小,即,要求时间复杂度和输入输出同阶。

算法.对文本T建立后缀自动机,和上一个问题相似,在构建自动机的过程中对每个状态计算第一次出现的终点firstpos。

假设我们收到了一个询问——字符串P。我们找到了它对应的状态t。

 显然应当返回firstpos(t)。还有哪些位置?我们考虑自动机中那些包含了字符串P的状态,即那些P是其后缀的状态。

 换言之,我们需要找出所有能通过后缀链接到达状态t的状态。

 因此,为了解决这一问题,我们需要对每个节点储存指向它的所有后缀链接。为了找到答案,我们需要沿着这些翻转的后缀链接进行DFS/BFS,从状

态t开始。

 这一遍历将在O(answer(P))时间内结束,因为我们不会访问同一状态两次(因为每个状态的后缀链接仅指向一个点,因此不可能有两条路径通往同一

状态)。

 然而,两个状态的firstpos值可能会相同,如果一个状态是由另一个拷贝而来。但这不会影响渐进复杂度,因为每个非拷贝得到的节点只会有一个拷贝。

 此外,你可以轻松地除去那些重复的位置,如果我们不考虑那些拷贝得来的状态的firstpos。事实上,所有拷贝得来的状态都被其“母本”状态的后缀链接

指向。因此,我们对每个节点记录标签is_clon,我们不考虑那些is_clon=true的状态的firstpos。这样我们就得到了answer(P)个不重复地状态。

查询不在文本中出现的最短字符串

问题.给定字符串S和字母表。要求找出一个长度最短的字符串,使得它不是S的子串。

复杂度要求.O(length(S)).

 算法.在字符串S的后缀自动机上进行动态规划。

 令d[v]为节点v的答案,即,我们已经输入了字符串的一部分,匹配到v,我们希望找出有待添加的最少字符数量,以到达一个不存在的转移。

 计算d[v]非常简单。如果在v处某个转移不存在,那么d[v]=1:用这个字符来“跳出”自动机,以得到所求字符串。

 否则,一个字符串无法达到要求,因此我们必须取所有字符中的最小答案:

原问题的答案等于d[t_0],而所求字符串可以用记录转移路径的方法得到。

求两个字符串的最长公共子串

问题.给定两个字符串S和T。要求找出它们的最长公共子串,即一个字符串X,它同时是S和T的子串。

  复杂度要求.O(length(S)+length(T)).

算法.我们对字符串S建立后缀自动机。

我们按照字符串T在自动机上走,查找它每个前缀在S中出现过的最长后缀。换句话说,对字符串T中的每个位置,我们都想找出S和T在该位置结束的最长公共子串。

为了实现这一点,我们定义两个变量:当前状态v和当前长度l。这两个变量描述了当前的匹配部分:其长度和状态,对应哪个字符串(如果不储存长度就无法确定这一点,因为一个状态可能匹配多个有不同长度的字符串)。

最初,p=t_0,l=0,即,匹配部分为空。

现在我们考虑字符T[i],我们希望找到这个位置的答案。

·        如果自动机中的状态v有一个符号T[i]的转移,我们可以简单地走这个转移,然后将长度l加一。

·        如果状态v没有该转移,我们应当尝试缩短当前匹配部分,为此应当沿着后缀链接走:
v=link(v).
在此情况下,当前匹配长度必须被减少,但留下的部分尽可能多。显然,应令l=len(v):
l=len(v).
若新到达的状态仍然没有我们想要的转移,那我们必须再次沿着后缀链接走,并且减少l的值,直到我们找到一个转移(那就返回第一步),或者我们最终到达了空状态-1(这意味着字符T[i]并未在S中出现,所以令v=l=0然后继续处理下一个i)。

原问题的答案就是l曾经达到的最大值。

这种遍历方法的渐进复杂度是O(length(T)),因为在一次移动中我们要么将l加一,要么沿着后缀链接走了若干次,每次都会严格减少l。因此,l总共的减少值之和不可能超过length(T),这意味着线性时间复杂度。

多个字符串的最长公共子串

问题.给出K个字符串S_1~S_K。要求找出它们的最长公共子串,即一个字符串X,它是所有S_i的子串。

  复杂度要求.O(∑length(S_I)*K).

算法.将所有S_i连接在一起成为一个新的字符串T,其中每个S_i后要加上一个不同的分隔符D_i(即加上K个额外的不同特殊字符D_1~D_K):

我们对字符串T构建后缀自动机。

 现在我们需要在后缀自动机找出一个字符串,它是所有字符串S_i的子串。注意到如果一个子串在某个字符串S_j中出现过,那么后缀自动机中存在一

条以这个子串为前缀的路径,包含分隔符D_j,但不包含其他分隔符D_1,...,D_j-1,D_j+1,...,D_k。

 因此,我们需要计算“可达性”:对自动机中的每个状态和每个字符D_i,计算是否有一条从该状态开始的路径,包含分隔符D_i,但不包含别的分隔符。

很容易用DFS/BFS或者动态规划实现。在此之后,原问题的答案就是字符串longest(v),其中v能够到达所有的分隔符。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值