zoukankan      html  css  js  c++  java
  • 【算法】字符串

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

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

    【字符串哈希】

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

    例题:【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
    ",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都是同理咯。

  • 相关阅读:
    HDU 1982 Kaitou Kid The Phantom Thief (1)
    HDU 1984 Mispelling4
    HDU 2546 饭卡
    HDU 1009 FatMouse' Trade
    在VC 中如何隐藏一个主程序窗口
    .菜单项
    SetClassLong,GetClassLong 动态改变光标
    .窗口捕获鼠标
    .主窗口向子控件发送消息
    线段树 1698 Just a Hook 区间set更新
  • 原文地址:https://www.cnblogs.com/onioncyc/p/6618877.html
Copyright © 2011-2022 走看看