比较全的字符串算法汇总

目录

  • KMP
  • AC自动机
    • AC自动机_引入
    • AC自动机的构建
    • AC自动机查找
    • 模板代码
    • 注意事项
    • 例题选讲
    • 另一种写法
  • hash&&trie&&manacher
  • SA后缀数组
    • 1、后缀数组作用
    • 2、后缀数组的构造
    • 3、 SA算法的用途
    • 4、例题:poj 3261 : Milk Patterns
  • 后缀树 (suffix-tree)
  • 后缀自动机(SAM)

大坑填完了!

KMP

做题需要脑筋急转弯

板子

for(int i=2;i<=n;i++){
    int j=next[i-1];
    for(;j&&s[j+1]!=s[i]) j=next[j];
    if(s[j+1]==s[i]) next[i]=j+1;
    else next[i]=0;
}

匹配:

int i=0,j=0;
    while(i<=len1){
        if(j==0||s1[i+1]==s2[j+1]){
            i++,j++;
        }else j=next[j];
        if(j==len2){
            printf("%d\n",i-len2+1);
            j=next[j];
        }
    }

AC自动机

AC自动机_引入

  • 对于k个模式串,我们要匹配一个文本串。

    如果采用建立k个next数组的方法(kmp),时间复杂度显然为O((mi+n)*k),不可接受。

  • 那我们就需要一种更简便的数据结构(误),来实现在可控时间范围内(O(n))内的匹配。
    除此之外,AC自动机还可用于多模式串匹配下的其他算法,如dp等、

AC自动机的构建

AC自动机的框架

  1. AC自动机的实质是一颗带失配指针的trie树
  2. fail指针是失配时要跳到的地方,每个节点都有一个fail指针
  3. 第一层的节点fail指针直接指向0号节点(虚拟节点)

如何建立AC自动机

这里采用构建fail图的方式,会比较好写

  • 对于节点a,它在trie树上有3个儿子,b,c,d。(这里节点编号即它的字母)
  • 然而它没有儿子e,但是当有另一个节点要访问下一个e时(下文解释),只有沿着fail不断跳到有e为止,还要写一个while
  • 于是把这个空节点赋值为它fail指针指向的节点的e节点
  • 这样其他节点就可以直接访问这个空节点了

伪代码 1:

    if not (tree[now].son[i])
    tree[now].son[i]=tree[tree[now].fail].son[i];

对于一个节点的fail值,应该是失配后要跳到的地方。
那么fail所指向的节点应该满足什么条件呢?

  1. 这个节点在trie上的单词(这个字母及以前)应该包含fail指向的节点的单词(这个字母及以前)
  2. 由(1)可得,这个字母应该等于fail所指向的字母

所以构建方法就出来了:
对于一个节点,它的父亲 以及 父亲的fail节点一定满足条件 1
所以只用找父亲的 fail的 与自己相同的 儿子就行了QwQ

伪代码 2:

    int nex=tree[now].son[i];
            tree[nex].fail=tree[tree[now].fail].son[i];

再加上bfs把所有单词遍历完 = 完整代码(建树)

void bfs(){
    queueq;
    for(int i=0;i<26;i++){
        if(tree[0].son[i]) {
            tree[tree[0].son[i]].fail=0; 
            q.push(tree[0].son[i]);
        }
    }   //首先,第一层的fail一定是0;
    while(!q.empty()){
        int now=q.front();
        q.pop();
        for(int i=0;i<26;i++){      //注意:所有字母
            if(tree[now].son[i]){
                int nex=tree[now].son[i];
                tree[nex].fail=tree[tree[now].fail].son[i];
                q.push(nex);    //有儿子 
            }else tree[now].son[i]=tree[tree[now].fail].son[i];   //没有这个儿子
        }
    }
}

AC自动机查找

这个就比较简单了。。
既然是 O(n) 的时间,那么肯定要枚举文本串 (逆 因 果 暴 论)

枚举文本串,对于每一位在AC自动机上跳。
如果当前单词已经没有文本串的下一个单词,直接跳到fail所指向的这个单词(有时候要跳很多次

但是不用。注意到,我们构建的并非fail树,而是fail图。

于是把这个空节点赋值为它fail指针指向的节点的e节点

所以直接用儿子就行了

枚举到每一个点时,沿着fail往上跳。
由于每个结尾点都在trie树的末尾(废话),而且满足

这个节点在trie上的单词(这个字母及以前)应该包含fail指向的节点的单词(这个字母及以前)

的性质,所以沿着fail往上跳,遇到有结尾的标记就++ans(也可以作其他处理)就行了。

下面是喜闻乐见的代码:

void search(){
    int now=0,len=strlen(m);
    for(int i=0;i

模板代码

模板题: Luogu p3796
我有独特的存字符串手段,不建议学(这都不重要)

#include
#include
#include
#include
using namespace std;
int read(){
    int x=0,pos=1;char ch=getchar();
    for(;!isdigit(ch);ch=getchar()) if(ch=='-') pos=0;
    for(;isdigit(ch);ch=getchar()) x=x*10+ch-'0';
    return pos?x:-x;
}
int n;char s[1001],m[1000051];
int ans[1001];
char an[201][201];int l[201];
struct ac{
    int son[30],fail,end;
}tree[1000001];
int tot=0;
void insert(int cnt){
    int now=0;int len=l[cnt];
    for(int i=0;iq;
    for(int i=0;i<26;i++){
        if(tree[0].son[i]) {
            tree[tree[0].son[i]].fail=0; 
            q.push(tree[0].son[i]);
        }
    }
    while(!q.empty()){
        int now=q.front();
        q.pop();
        for(int i=0;i<26;i++){
            if(tree[now].son[i]){
                int nex=tree[now].son[i];
                tree[nex].fail=tree[tree[now].fail].son[i];
                q.push(nex);
            }else tree[now].son[i]=tree[tree[now].fail].son[i];
        }
    }
}
void search(){
    int now=0,len=strlen(m);
    for(int i=0;imaxn) maxn=ans[i];
        }
        printf("%d\n",maxn);
        for(int i=1;i<=n;i++){
            if(ans[i]==maxn){
                for(int j=0;j

注意事项

  1. AC自动机是离线数据结构
  2. 一定要建fail图!!!

其实好像也没几个要注意的(逃

例题选讲

  1. 点我QAQ

    题意:从一个长度不超过10^5的字符串S中删除一些单词,输出最后的S
    注意,删除一个单词后可能会导致S中出现另一个列表中的单词

    10^5,可以O(n)
    用两个栈模拟即可,主要靠对AC自动机的理解与运用

    于是把我给的模板中的search魔改一下就OK QwQ
    参考代码:

#include
#include
#include
#include
using namespace std;
int read(){
    int x=0,pos=1;char ch=getchar();
    for(;!isdigit(ch);ch=getchar()) if(ch=='-') pos=0;
    for(;isdigit(ch);ch=getchar()) x=x*10+ch-'0';
    return pos?x:-x;
}
int n;char s[100051],m[100051];
int l[100051];
struct ac{
    int son[30],fail,end;
}tree[100011];
int tot=0;
void insert(int cnt){
    int now=0;int len=l[cnt];
    for(int i=0;iq;
    for(int i=0;i<26;i++){
        if(tree[0].son[i]) {
            tree[tree[0].son[i]].fail=0; 
            q.push(tree[0].son[i]);
        }
    }
    while(!q.empty()){
        int now=q.front();
        q.pop();
        for(int i=0;i<26;i++){
            if(tree[now].son[i]){
                int nex=tree[now].son[i];
                tree[nex].fail=tree[tree[now].fail].son[i];
                q.push(nex);
            }else tree[now].son[i]=tree[tree[now].fail].son[i];
        }
    }
}
int s1[100010],s2[100051];
void search(){
    int now=0,len=strlen(m);
    int top=0;
    for(int i=0;i
  1. 题意:

    贝西在玩一款游戏,该游戏只有三个技能键 “A”“B”“C”可用,但这些键可用形成N种(1 <= N<= 20)特定的组合技。第i个组合技用一个长度为1到15的字符串S_i表示。
    当贝西输入的一个字符序列和一个组合技匹配的时候,他将获得1分。特殊的,他输入的一个字符序列有可能同时和若干个组合技匹配,比如N=3时,3种组合技分别为"ABA", "CB", 和"ABACB",若贝西输入"ABACB",他将获得3分。
    若贝西输入恰好K (1 <= K <= 1,000)个字符,他最多能获得多少分?

    题意好像很浅显,就是告诉你一堆模式串,叫你求出限定长度的最大匹配

    反向思维是很重要的,就像Poi2000 病毒这道题。
    那我们不妨设想:如果已经求出了这个串,该如何匹配?
    答案是一直跳son,并对这个节点跳一次fail算答案。

    那么我们就可以把每个点的贡献算出来(跳fail能得到的答案),在AC自动机上DP。

    设计状态\(f[i][j]\),表示长度为\(i\)的字符串在跳到\(j\)这个点时这个点以及前面(可能是环,注意顺序就好)的最大答案。
    那么显然有

    \(f[i][tree[j].son[s]]=max(f[i][tree[j].son[s]],ans[tree[j].son[s]]+f[i-1][j])\)

    解释一下,对于某个节点进行拓展,可以更新它儿子的f值。 如果取这个儿子进行拓展,答案是自己之前长度为j-1的最大值加上这个儿子的贡献,否组不取,枚举下一个儿子。

    边界:\(f[0][u]\)=0 长度为零,答案自然为0
    满足无后效性的方法:计算某个节点的f值的时候,要用到f[i-1][j],i就从1~maxlen枚举
    son的访问顺序倒无所谓,反正用到的son的f是上一轮计算过的,按插入顺序枚举就好。

    代码:

    #include
    #include
    #include
    #include
    using namespace std;
    int read(){
        int x=0,pos=1;char ch=getchar();
        for(;!isdigit(ch);ch=getchar()) if(ch=='-') pos=0;
        for(;isdigit(ch);ch=getchar()) x=x*10+ch-'0';
        return pos?x:-x;
    }
    int n;char s[2001],m[1000051];int f[1000051],dp[1001][10001];int l[2001];
    struct ac{
        int son[4],fail,end,fl,q[4];
    }tree[1000001];
    int tot=0;
    void insert(int cnt){
        int now=0;int len=l[cnt];
        for(int i=0;iq;
        for(int i=1;i<=3;i++){
            if(tree[0].son[i]) {
                tree[tree[0].son[i]].fail=0; 
                q.push(tree[0].son[i]);
            }
        }
        while(!q.empty()){
            int now=q.front();
            q.pop();
            for(int i=1;i<=3;i++){
                if(tree[now].son[i]){
                    int nex=tree[now].son[i];
                    tree[nex].fail=tree[tree[now].fail].son[i];
                    tree[tree[tree[now].fail].son[i]].fl=nex;
                    q.push(nex);
                }else tree[now].son[i]=tree[tree[now].fail].son[i];
            }
            f[now]+=f[tree[now].fail];
        }
    }
    int maxlen;
    int DP(){
        for(int i=0;i<=maxlen;i++){
            for(int j=1;j<=tot;j++){
                dp[i][j]=-19260817;
            }
        }
        for(int i=1;i<=maxlen;i++){
            for(int j=0;j<=tot;j++){
                for(int s=1;s<=3;s++){
                    dp[i][tree[j].son[s]]=max(dp[i][tree[j].son[s]],f[tree[j].son[s]]+dp[i-1][j]);
                }
            }
        }
        int mn=0;
        for(int i=0;i<=tot;i++){
            mn=max(mn,dp[maxlen][i]);
        }
        return mn;
    }
    int main(){
        n=read();scanf("%d",&maxlen);
        tot=0;
        for(int i=1;i<=n;i++){
            scanf("%s",s);
            int len=strlen(s);
            l[i]=len;
            insert(i);
        }
        bfs();
        printf("%d",DP());
        return 0;
    } 

另一种写法

luogu模板2,更快,但是第一种大部分时候够用

#include
#include
#include
#include
#include
using namespace std;
int read(){
    int x=0,pos=1;char ch=getchar();
    for(;!isdigit(ch);ch=getchar()) if(ch=='-') pos=0;
    for(;isdigit(ch);ch=getchar()) x=x*10+ch-'0';
    return pos?x:-x;
}
int n;char s[200001],m[2000051];int pans[10000001],siz[10000051];
int ans[200001];
int l[200001],lnk[200001];
struct ac{
    int son[30],fail,end;
}tree[10000001];
struct node{
    int v,nex;
}edge[1000001];
int tope=0,head[1000001];
void add(int from,int to){
    edge[++tope].v=to;
    edge[tope].nex=head[from];
    head[from]=tope;
}
int tot=0;
void insert(int cnt){
    int now=0;int len=l[cnt];
    for(int i=0;ist;
void bfs(){
    queueq;
    for(int i=0;i<26;i++){
        if(tree[0].son[i]) {
            tree[tree[0].son[i]].fail=0; 
            q.push(tree[0].son[i]);
        }
    }
    while(!q.empty()){
        int now=q.front();
        q.pop();
        for(int i=0;i<26;i++){
            if(tree[now].son[i]){
                int nex=tree[now].son[i];
                tree[nex].fail=tree[tree[now].fail].son[i];
                q.push(nex);
            }else tree[now].son[i]=tree[tree[now].fail].son[i];
        }
        st.push(now);
    }
}
void search(){
    int now=0,len=strlen(m);
    for(int i=0;i

hash&&trie&&manacher

算法比较浅显,准备之后写综合题做题记录吧

放个manacher板子方便复习:

#include
#include
#include
using namespace std;
char a[11000011],s[22000817];
int p[22000817],ans,n;
inline void init(){
    s[0]=s[1]='#';
    for(int i=0;ib?b:a;
}
void manacher(){
    int mr=0,mid=0;
    for(int i=0;imr){
            mr=p[i]+i;
            mid=i;
        } 
        if(p[i]>ans) ans=p[i];
    }
}
int main(){
    scanf("%s",a);
    n=strlen(a);
    init();
    manacher();
    printf("%d",ans-1);
    return 0;
}

SA后缀数组

1、后缀数组作用

主要用于解决最长公共前缀(lcp)问题,大多数时候此类问题都可以用sam(后缀自动机)来解决。不过应为sa算法相对更加优秀的时空复杂度,在大数据集上可以防止TMLE。

2、后缀数组的构造

  1. 首先声明几个变量。
    1、Str :需要处理的字符串(长度为Len)
    2、Suffix[i] :Str下标为i ~ Len的连续子串(即后缀)
    3、Rank[i] : Suffix[i]在所有后缀中的排名
    4、SA[i] : Rank的逆运算,就是排名第i大的字符串是啥。
  2. 构造方式大概有两种:DC3和倍增
    DC3 时间O(n),空间O(3*n) 常数大 (看脸)
    倍增 时间O(nlogn),常数小

    这里介绍倍增 其实是我只会倍增

考虑暴力
对于一个后缀,想要直接构造出它的rank可以用快排(\(nlogn\)排序+每次\(O(n)\)比较)
这种是\(n^2\log n\)的时间复杂度,显然不能接受。

使用倍增的思想 设\(SubStr(i, len)\)为从第\(i\)个字符开始,长度为\(len\)的字符串

我们可以把第k轮\(SubStr(i, 2^k)\)看成是一个由SubStr\((i, 2k−1)\)和SubStr\((i + 2k−1, 2k−1)\)拼起来的东西。
这两个长度而2^k−1的字符串是上一轮计算过的,当然上一轮的\(rank\)也知道。
那么把每个这一轮的字符串都转化为这种形式,并且大家都知道字符串的比较是从左往右,左边和右边的大小我们可以用上一轮的\(rank\)表示。
这就可以视为一些第一关键字和第二关键字比较大小再把这些两位数重新排名就是这一轮的\(rank\)

tips:如果使用传统排序方法(快排)那么时间复杂度就是nlog^2n,在这里我们可以使用基数排序,将时间复杂度降为\(nlogn\)(桶的大小为字符集)。

#include
#include
#include
#include
using namespace std;
char s[1000011];
int len,n;
int SA[1000011];
int x[1000011],y[1000011],t[1000011];
int p[1000011];
inline void GET_SA(){
    //y是第二关键字,x是第一关键字
    //SA是排名为下标的子串的字符串中的位置
    //y和SA:排名为下标,映射位置
    //x:位置为下标,映射排名
    int m=122;n=len;//m->字符集
    for(int i=1;i<=n;++i) t[x[i]=s[i]]++;  
    for(int i=1;i<=m;++i) t[i]+=t[i-1];
    for(int i=n;i;--i) SA[t[x[i]]--]=i;     //先求出k=0时候的SA       
    /*for(int i=1;i<=n;i++){
            printf("%d ",SA[i]);
        }
    putchar('\n');*/
    int num=0;
    for(int k=1;k<=n&&num<=n;k<<=1){
        num=0;
        for(int i=1;i<=m;++i) t[i]=0;   
        for(int i=n-k+1;i<=n;++i) y[++num]=i;   //对于没有第二关键字,第二关键字优先(长度小的在前面
        for(int i=1;i<=n;++i) if(SA[i]>k) y[++num]=SA[i]-k; //其余:如果有第二关键字,按第二关键字的顺序(sa)存入y
        for(int i=1;i<=n;++i){
            t[x[y[i]]]++;
        }   //可以用p[i]=x[y[i]]来卡常
        for(int i=1;i<=m;++i) t[i]+=t[i-1]; 
        for(int i=n;i;--i) SA[t[x[y[i]]]--]=y[i];   
            //桶排计算SA
            //按第二关键字的顺序访问第一关键字,要倒序(越大的桶排顺序越后)
            //x已经计算好了
        /*for(int i=1;i<=n;i++){
            printf("%d ",SA[i]);
        }
        putchar('\n');*/
        swap(x,y);
        x[SA[1]]=1;num=1;
        for(int i=2;i<=n;++i){
            x[SA[i]]=(y[SA[i]]==y[SA[i-1]]&&y[SA[i-1]+k]==y[SA[i]+k])?num:++num;
        }   //按照SA的顺序计算下一轮的x,模拟即可,注意边界
        m=num;
    }
}
void prt(int x){
    int tmp[20],*t =tmp;
    for(;x;x/=10)*t++=x%10+'0';
    if(t==tmp)putchar('0');
    else for(--t;t>=tmp;--t)putchar(*t);
    putchar(' ');
}
int main(){
    scanf("%s",s+1);
    len=strlen(s+1);
    GET_SA();
    for(int i=1;i<=n;++i){
        prt(SA[i]);
    }
    return 0;
}

板子有点难背。。。

3、 SA算法的用途

  1. 同样先是定义一些变量
    \(Heigth[i]\) : 表示\(Suffix[SA[i]]\)\(Suffix[SA[i - 1]]\)的最长公共前缀,也就是排名相邻的两个后缀的最长公共前缀 。

    $H[i] \(: 等于\)Height[Rank[i]]\(,也就是后缀\)Suffix[i]\(和它前一名的后缀的最长公共前缀 。 而两个排名不相邻的最长公共前缀定义为**排名在它们之间**的\)Height$的最小值。

    感性理解一下:rank相邻的两个后缀是从后往前变的,取min

  2. 如何高效的计算\(height\)数组
    可以知道\(h[i]>=h[i-1]-1\);
    可以思考一下。
    然后按照hi的顺序来算\(height[i]\),暴力算

  3. 简单应用
    (1. 一个串中两个串的最大公共前缀是多少?
    (2. 一个串中可重叠的重复最长子串是多长?
    (3. 一个串中不可重叠的重复最长子串是多长? (poj1743)

    1,这就是height啊,用rmq处理即可。
    2,ON扫过去,看height的最大值即可。
    3,二分答案,把所有的height按照k分组,保证每组间的height大于k,然后查看每组最大的sa值和最小的sa值相差是不是超过了k,有一组超过答案就合法

  4. 进阶应用

一个字符串不相等的子串的个数是多少?

每个子串一定是某个后缀的前缀,那么原问题等价于求所有后缀之间的不相同的前缀的个数。
可以发现每一个后缀Suffix[SA[i]]的贡献是Len - SA[i] + 1,但是有子串算重复。
重复的就是Heigh[i]个与前面相同的前缀,那么减去就可以了。最后,一个后缀Suffix[SA[i]]的贡献就是Len - SA[i] + 1 - Height[i]。

4、例题:poj 3261 : Milk Patterns

命运石之传送

题意:可重叠的\(k\)次最长重复子串

容易想到二分长度(重叠\(>k\)次的也一定重叠了\(k\)次,满足单调性),然后分组查看每组中有没有足够多数量的字符串

后缀树 (suffix-tree)

咕咕咕(

后缀自动机(SAM)

写不出通俗易懂的博客(直球)

于是挂个好懂的博客,写一写做题记录吧(还是需要耐心看)

DALAO's blog

想了想还是记录一下构建方法

(上面那篇博客里有图解,引入也很好,我就不画图了

令旧串等于当前字符c前面的串,

case1:

旧串的后缀没有包含c的情况,此情况旧串的后缀和c拼接成一个新的endpos,parent树上父亲是旧串后缀endpos节点

case2:

旧串的后缀有包含c的情况,一种是完全包含新的endpos,另一种是有不同

举个例子:

  1. aa

    加入第二个a的时候显然形成了aa和a,前面有一个a,加入之后endpos是1和2,而aa的endpos是2,包含

  2. aabab

    加入第五个字符b的时候会形成aabab:{5} ,ab:{3,5} (只记录最长的

    而前面3处最长的显然不等于ab,是aab:{3}

    这种情况就需要拆点新建节点

怎么判断呢?设跳到的节点是p,出边是q,判断依据是 \(len(q)==len(p)+1\)

因为只能加一个c才能使endpos完全包含

来看上面的两个例子:(一号节点是根节点,代表空串)

  1. \(len(1)=0,len(2)=1\)

    \(len(1)+1=len(2)=1\)

  2. \(p=2,q=4\)

    (2连向4的b边)

    \(len(q)=3\)(aab)

    \(len(p)=1\)(a)

    a加上一个字符应该是ab,但是出现了aab不能包含其endpos!

    此时\(len(q)=len(p)+2\)

情况一很好处理,直接把fa(np)置为q即可,参考parent树的定义

情况二不好处理,借用上面博客的一句话:

现在,我们要想办法将其中一个类的子串移到另一个节点上,只保留其中一类的字符串,让 qendpos 可以得到定义。

考虑新建一个节点\(nq\),让endpos多了一个n(c的位置)的字符串转移到这个点,原来的q只存endpos不变的

\(len(nq)=len(p)+1\)

\(len(fa(nq))

\(fa(np)=nq\)

然后跳fa,\(for(;p且dian[p].ch[c]==q;p=dian[p].fa)dian[p].ch[c]=nq;\)

为什么不往上跳了?感性地理解,此时后缀变短了,endpos就被包含了

其实是此时的c儿子一定是q的某个祖先,而nq也一定是这个祖先的儿子

至此就构建完了

板子是结构体版的,

#include
#include
#include
#include
#define ll long long
using namespace std;
const int N = 1000001;
char s[N];
int n,w[N<<1];
struct Node{
    int ch[26];
    int len,fa;
    Node(){memset(ch,0,sizeof(ch));len=fa=0;}
}t[N<<1];//开两倍空间!
int las=1,tot=1;//1号是空节点
struct node{
    int v,nex;
}edge[N<<1];
int head[N<<1],tope=0; 
ll ans=0;
inline void adde(int u,int v){
    edge[++tope].v=v;
    edge[tope].nex=head[u];
    head[u]=tope;
}
inline void add(int c){
    int p=las,np=las=++tot;
    w[tot]=1;
    t[np].len=t[p].len+1;
    for(; p&&!t[p].ch[c];p=t[p].fa) t[p].ch[c]=np;
    if(!p) t[np].fa=1;
    else{
        int q=t[p].ch[c];
        if(t[q].len==t[p].len+1){
            t[np].fa=q;
        }else{
            int nq=++tot;
            t[nq]=t[q];//复制节点信息
            t[nq].len=t[p].len+1;//只加上了c的情况 
            t[q].fa=nq;//q的fa是拆出来的点,拆出来的点的endpos分成了q和最后点(包含n) 
            t[np].fa=nq;//最后点的fa是拆出来的点 
            //t[nq].fa=t[q].fa已经在前面结构体转移的时候做过 
            for(;p&&t[p].ch[c]==q;p=t[p].fa) t[p].ch[c]=nq;
        }
    }
} 
void dfs(int now){
    for(int i=head[now];i;i=edge[i].nex){
        int v=edge[i].v;
        dfs(v);
        w[now]+=w[v];
    }
    if(w[now]!=1) ans=max(ans,(ll)(w[now])*(ll)(t[now].len));
}
int main(){
    scanf("%s",s);
    int n=strlen(s);
    for(int i=0;i

听说数组版会快一点?

之后会补做题记录(相信我,咕咕咕

你可能感兴趣的:(比较全的字符串算法汇总)