【算法】字符串

把字符串原样复制一遍放在后面是惯用套路,此时字符串数组开两倍!

★字符串算法的核心是构造失配指针!

【字符串哈希】

双蛤习取模保险,毕竟连自然溢出都是能卡的……

例题:【CodeForces】961 F. k-substrings 字符串哈希+二分

用于O(1)判断两个字符串是否相等:对于s[i~j],哈希值为h[j]-h[i]*p[j-i+1]。

用于O(log n)判断两个字符串大小(字典序),方法是二分求LCP,比较下一位。

用于O(n log2n )求后缀数组。

字符串hash以及7大问题

for(int i=1;i<=n;i++)h[i]=(h[i-1]*base+s[i])%p;
for(int i=1;i<=n;i++)H[i]=(H[i-1]*Base+s[i])%P;
if(h[y]-h[x-1]==h[b]-h[a-1]&&H[y]-H[x-1]==H[b]-H[a-1]);
View Code

 

【KMP】

模板:【洛谷】3375 KMP字符串匹配

KMP解决的是线性时间在模式串A中找到匹配串B的问题。

对于匹配串B的前i个字符构成的子串,既是它的后缀又是它的前缀的字符串中(它本身除外),最长的长度记作fail[i]。

比较时,如果A[i]=B[j+1],则j++,否则j=fail[j]。

fail[i]的实际含义就是此处匹配而下处失配时往前跳到一样的位置(即前缀=后缀),显然fail[1]=0(1处匹配2处失配,只能跳到0处),fail[0]没有意义。

预处理fail数组也可以视为匹配,如果B[i]=B[j+1],则fail[i]=++j,否则j=fail[j],继续比较。

(换一种角度看,当前需要计算fail[i],已知fail[i-1],判断fail[i]=fail[i-1]+1是否成立,否则判断fail[i]=fail[fail[i-1]]+1……)

记得kmp的预匹配必须从2开始循环,这样2可以和1比较,避免追上(比较到自身)。j=0就无处可跳了,不必再跳。

blog:http://www.matrix67.com/blog/archives/115

【BZOJ】1355 [Baltic2009]Radio Transmission 循环节

【BZOJ】3670 [Noi2014]动物园 KMP树

★upd:KMP的核心是强大的fail数组,表示的是后缀等于前缀的最大长度,这个性质非常强,这里只用于匹配时的快速失配跳后重新匹配。

 

#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
const int maxn=1000010,maxm=1010;
char A[maxn],B[maxm];
int p[maxm],n,m;
int main()
{
    scanf("%s%s",A+1,B+1);
    n=strlen(A+1);m=strlen(B+1);
    p[1]=0;
    int j=0;
    for(int i=2;i<=m;i++)
     {
         while(j>0&&B[j+1]!=B[i])j=p[j];
         if(B[j+1]==B[i])j++;
         p[i]=j;
     }
    j=0;
    for(int i=1;i<=n;i++)
     {
         while(j>0&&B[j+1]!=A[i])j=p[j];
         if(B[j+1]==A[i])j++;
         if(j==m)
          {
              printf("%d\n",i-j+1);
              j=p[j];
         }
     }
    for(int i=1;i<m;i++)printf("%d ",p[i]);
    printf("%d",p[m]);
    return 0;
}
View Code

 

【AC自动机】识别字符串的自动机

AC自动机是对若干模式串O(m*26)建立trie和fail边,从而实现O(n)从新串中匹配到所有存在的模式串。

AC自动机中,不存在的节点直接指向fail节点处,存在的节点fail[ch[u][c]]=ch[fail[u]][c]

询问的时候按位直接转移,失配会自动跳跃不用写出来,复杂度O(n)。

如果每次都要询问到0点的所有fail,须标记访问过的点不再访问,否则复杂度不对,参考aaaaa。

#include<cstdio>
#include<cstring>
#include<queue>
using namespace std;
const int maxn=1000010;
int ch[maxn][26],val[maxn],fail[maxn],sz,ans;
queue<int>Q;
char s[maxn];

void insert(char *s){
    int u=0,n=strlen(s);
    for(int i=0;i<n;i++){
        int c=s[i]-'a';
        if(!ch[u][c])ch[u][c]=++sz;
        u=ch[u][c];
    }
    val[u]++;
}
void AC_build(){
    for(int c=0;c<26;c++)if(ch[0][c])Q.push(ch[0][c]);
    while(!Q.empty()){
        int u=Q.front();Q.pop();
        for(int c=0;c<26;c++){
            if(!ch[u][c])ch[u][c]=ch[fail[u]][c];else{
                Q.push(ch[u][c]);
                fail[ch[u][c]]=ch[fail[u]][c];
                //last[ch[u][c]]=val[fail[ch[u][c]]]?fail[ch[u][c]]:last[fail[ch[u][c]]];
            }
        }
    }
}

void work(int u){if(fail[u]&&~val[fail[u]])work(fail[u]);ans+=val[u],val[u]=-1;}
void find(char *s){
    int n=strlen(s);
    int u=0;
    for(int i=0;i<n;i++){
        u=ch[u][s[i]-'a'];
        if(~val[u])work(u);
    }
}
int main(){
    int n;
    scanf("%d",&n);
    for(int i=1;i<=n;i++){
        scanf("%s",s);
        insert(s);
    }
    AC_build();
    scanf("%s",s);
    find(s);
    printf("%d",ans);
    return 0;
}
View Code

 

trie的初始化可以用即化即用的方法,即访问到才初始化其子节点,保持旧版本和新版本有一层空白间隔。

last只能优化常数。

★upd:任何字符串数据结构都依赖于强大的fail机制。AC自动机的fail会带到最近的满足后缀=前缀的节点处,同时一个点在fail树上到根的路径就是匹配了这个点代表串的所有在AC-aho上的后缀。

 

【回文自动机】识别回文子串的自动机(PAM),又称”回文树“。

初始节点:ch[1]表示len=-1下接奇数串(为了方便,这里用1存节点-1),ch[0]表示len=0下接偶数串,0点指向-1点(之后拓展的所有节点都会先fail到0点再到1点)。

节点:每个点表示一个本质不同的回文串(从根到点组成的字符串是回文串中从中间到右端的串)

fail指针:每个点fail到相同后缀的次短回文串节点(显然最短到点0,然后才到-1),由于回文的性质次短回文子串代表节点一定已经出现过。

线性构造:(计算len)新加入一个字符a时,若a-1的最长回文串往前到b,则a的最长回文串至多到b-1,而能否到b-1取决于(s[a]==s[b-1])的真假。

所以从a-1代表的节点y开始不断fail直至满足s[a]==s[b-1]为止,就计算出了a的最长回文串(ch[x].len=ch[y].len+2),如果没有对应节点就新建(ch[y].t[x])。

(计算fail)若新建节点,构造x的fail指针只需从ch[y].fail开始再次找到满足s[a]==s[b-1]的为止

复杂度O(n)。

注意:记得sz=1

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxn=100010;
int fail[maxn],len[maxn],ch[maxn][300],n,length=0,nownode,sz;
char s[maxn];
int getfail(int x){while(s[length-len[x]-1]!=s[length])x=fail[x];return x;}
void insert(){
    int y=s[++length]-'a';
    int x=getfail(nownode);
    while(!ch[x][y]){
        len[++sz]=len[x]+2;
        fail[sz]=ch[getfail(fail[x])][y];
        ch[x][y]=sz;
    }
    nownode=ch[x][y];
}
int main(){
    scanf("%s",s+1);
    n=strlen(s+1);
    len[0]=0;fail[0]=1;
    len[1]=-1;fail[1]=1;
    sz=1;//!!!
    for(int i=1;i<=n;i++)insert();
    int ans=0;
    for(int i=1;i<=sz;i++)ans=max(ans,len[i]);
    printf("%d",ans);
    return 0;
}
View Code

 

应用:

1.每个点的访问次数是该回文串作为最长回文串的次数,由fail边反向建新树就可以由子树得到该回文串所有信息。

例题:【BZOJ】3676: [Apio2014]回文串

 

一点心得:其实字符串自动机写多了就会发现,都是一样的。

回文自动机和SAM是一样……一样是记录本质不同的回文串(子串),一样是n个点n条边构成n^2个串。

节点的本质一样是Right集合,不过这里的不同在于回文自动机的Right集合是所有以该串为最长回文串的右端点,所以一个串的出现次数是子树的和。

fail边一样是前面删字符直至能继续下去。

 

【后缀数组】SA

n 字符串长度 m字符值为1~m

x 字符值数组/名次数组(x[i]表示后缀i的对应名次)

y 第二关键字排名对应后缀

sa 第一关键字排名对应后缀(总排名)

base 基排数组 base[x[.]] 取排名 sa[base[x[.]]--]=. 排名赋值

因为后缀一定不可能相同,所以暂时相同时的排名先后没有影响。

每次基排赋给SA对于同组都是先赋值名次越低,这就是再根据第二关键字排名的本质。

x数组本质上是sa对应的rank数组,主要目的是判重和记录最新排名以备下一次基排。

x数组可以把最新排名SA中相同的挑出来赋给同一个排名值。

基排赋值给SA时记得自减!

过程:

初始基排得到SA

倍增

  根据SA排出第二关键字y

  根据y的倒序再次排序SA

  根据原x和新的sa更新x

END

最后的x数组就是rank数组

计算LCP:h[i]表示SA中后缀i和后缀i-1的最长公共前缀。

按照h[i]≥h[i-1]-1,到SA[1]时自然会是0,不用担心。

void build_sa(int m)
{
    //初始基排-4步 
    for(int i=1;i<=m;i++)base[i]=0;//初始化 
    for(int i=1;i<=n;i++)base[x[i]=s[i]+1]++;//累积 
    for(int i=2;i<=m;i++)base[i]+=base[i-1];//叠加排名 
    for(int i=n;i>=1;i--)sa[base[x[i]]--]=i;//排名赋值(愈前愈前,但无所谓)
    for(int k=1;k<=n;k<<=1)//倍增 
     {
         int p=0;
         //排序第二关键字 
         for(int i=n-k+1;i<=n;i++)y[++p]=i;//没有第二关键字默认为$ 
         for(int i=1;i<=n;i++)if(sa[i]>k)y[++p]=sa[i]-k;//根据sa决定第二关键字排名,注意k即以后才能作为第二关键字 sa[i]-k取对应第一关键字(后缀)
        //排序第一关键字 
         for(int i=1;i<=m;i++)base[i]=0;
         for(int i=1;i<=n;i++)base[x[i]]++;
         for(int i=2;i<=m;i++)base[i]+=base[i-1];
         for(int i=n;i>=1;i--)sa[base[x[y[i]]]--]=y[i];//根据y顺序(倒)赋值SA 
         //把x放进y,然后更新x
        swap(x,y);
        p=1;x[sa[1]]=1; 
        for(int i=2;i<=n;i++)
         x[sa[i]]=y[sa[i-1]]==y[sa[i]]&&y[sa[i-1]+k]==y[sa[i]+k]?p:++p;//判重 
        if(p>=n)break;//排名各不相同即退出 
        m=p;
     }
    int k=0;
    for(int i=1;i<=n;i++)
     {
         if(k)k--;
         int j=sa[x[i]-1];//j是i在SA中的上一个后缀 
         while(s[i+k]==s[j+k])k++;
         h[x[i]]=k;
     }
}
View Code

算法合集之《后缀数组——处理字符串的有力工具》

【后缀自动机】识别子串的自动机。

专题链接

【序列自动机】识别子序列的自动机。

对于字符串,f[x][c]表示第x位后的第一个字符c的位置。|a|为字符集大小。

O(n|a|)构造:对于当前x位的字符c,上一个字符c的位置pre[c],使ch[y][c]=x,y=pre[c]~x-1。

    for(int i=1;i<=m;i++){
        int c=s[i]-'a';
        for(int j=i-1;j>=pre[c];j--)ch[j][c]=i;
        pre[c]=i;
    }
View Code

O(n log |a|)构造:从后往前扫,那么每次的操作就是复制数组后修改一个字符的数值,用可持久化线段树维护。

【trie】字典树

论文:《浅析字母树在信息学竞赛中的应用》

结构:数组存储式(空间大,时间小),链表存储式(空间小,时间大)

功能:

1.串的快速检索

2.串排序

3.从串中快速匹配单词

4.最长公共前缀(LCP)=两点的LCA

未完待续……

【自动机的本质】

字符串自动机的本质:所有字符串自动机的本质都是【节点】【Trans边】【Fail边】,自动机是一个或几个字符串建出来的,一个字符串可以在自动机上匹配到一些节点,结合自动机建串产生一些特殊的性质。

节点是匹配的对象。

Trans边是在匹配字符串后面加字母。

Fail边是在前面减字母,使得到达一个新状态。

一、KMP:单字符串匹配自动机

用模板串A建自动机,串B匹配。

①节点是串A的前缀。

②Trans边接串A下一个字符,不能接则失配。

③Fail边是在前面减字符,会发现转移到的点恰好就叫【最长的满足前缀=后缀的前缀右端点】。

于是KMP的fail数组就出现了所谓最长的“前缀=后缀”的长度这种含义。

再考虑KMP如何建自动机,只需要建Fail边。依赖于上一个节点的Fail,如果加一个字符还可以就继承,否则继续fail(继续减字符)直到满足。

二、AC-Aho:多字符串匹配自动机

用模板串集合建AC自动机,串B匹配。

①节点是本质不同的串前缀。

②Trans接26个字符转移到新的状态。

构造:直接依赖于Trie即可。

③Fail边是在前面减字符。

构造:将Trie从根开始用队列BFS,每个点的Fail依赖于上一个点。

这里有一个很厉害的优化,就是不匹配时直接用Trans边转移到(原来需要不断Fail到的)位置。

这样,如果匹配就Fail到上一个点的Fail位置+c处。如果不匹配就直接指向上一个点的Fail位置+c处,根据传递性能最直接到匹配的位置。

 

SAM和PAM都是同理咯。

 

posted @ 2017-03-25 22:47  ONION_CYC  阅读(514)  评论(0编辑  收藏  举报