zoukankan      html  css  js  c++  java
  • KMP 求最小循环节

    转载自:https://www.cnblogs.com/chenxiwenruo/p/3546457.html

    KMP模板,最小循环节

     

    下面是有关学习KMP的参考网站

    http://blog.csdn.net/yaochunnian/article/details/7059486

    http://blog.csdn.net/v_JULY_v/article/details/6111565

    http://blog.csdn.net/v_JULY_v/article/details/6545192

    http://blog.csdn.net/oneil_sally/article/details/3440784

    http://billhoo.blog.51cto.com/2337751/411486

     

    先说说next数组的含义:

    next[i]就是前面长度为i的字符串前缀和后缀相等的最大长度,也即索引为i的字符失配时的前缀函数。

    下面几个版本的next函数,除了next[0]不同外(版本一中为-1,版本二中为0),其余无差别

     

    一:KMP算法模板

    版本一:

    //求str对应的next数组
    void getNext(char const* str, int len)
    {
        int i = 0;
        next[i] = -1;
        int j = -1;
        while( i < len )
        {
            if( j == -1 || str[i] == str[j] )   //循环的if部分
            {
                ++i;
                ++j;
                //修正的地方就发生下面这4行
                if( str[i] != str[j] ) //++i,++j之后,再次判断ptrn[i]与ptrn[j]的关系
                    next[i] = j;      //之前的错误解法就在于整个判断只有这一句。
                else
                    next[i] = next[j];  //这里其实是优化了后的,也可以仍是next[i]=j
                //当str[i]==str[j]时,如果str[i]匹配失败,那么换成str[j]肯定也匹配失败,
                //所以不是令next[i]=j,而是next[i] = next[j],跳过了第j个字符,
                //即省去了不必要的比较
                //非优化前的next[i]表示前i个字符中前缀与后缀相同的最大长度
            }
            else                                 //循环的else部分
                j = next[j];
        }
    }
    
    //在目标字符串target中,字符str出现的个数
    //n为target字符串的长度,m为str字符串的长度
    int kmp_match(char *target,int n,char *str,int m){
        int i=0,j=0;  //i为target中字符的下标,j为str中字符的下标
        int cnt=0;   //统计str字符串在target字符串中出现的次数
        while(i<=n-1){
            if(j<0||target[i]==str[j]){
                i++;
                j++;
            }
            else{
                j=next[j]; //当j=0的时候,suffix[0]=-1,这样j就会小于0,所以一开始有判断j是否小于0
            }
    
            //str在target中找到匹配
            if(j==m){
                cnt++;
                j=next[j];
            }
        }
        return cnt;
    }
    //在目标字符串target中,若存在str字符串,返回匹配成功的第一个字符的位置
    int kmp_search(char *target,int n,char *str,int m){
        int i=0,j=0;  //i为target中字符的下标,j为str中字符的下标
        int cnt=0;   //统计str字符串在target字符串中出现的次数
        while(i<n && j<m){
            if(j<0||target[i]==str[j]){
                i++;
                j++;
            }
            else{
                j=suffix[j]; //当j=0的时候,suffix[0]=-1,这样j就会小于0,所以一开始有判断j是否小于0
            }
        }
        if(j>=m)
            return i-m;
        else
            return -1;
    }
    View Code

    版本二(算法导论):

    //这里的next和前面一样,next[i]就是前面长度为i的字符串前缀和后缀相等的长度,
    //即索引为i的字符失配时的前缀函数
    void getNext(char *str,int m){
        memset(next,0,sizeof(next));
        next[1]=0;
        int k=0;
        for(int i=2;i<=m;i++){
            while(k>0 && str[k]!=str[i-1])
                k=next[k];
            if(str[k]==str[i-1])
                k++;
            next[i]=k;
        }
    }
    //n为target字符串的长度,m为str字符串的长度,统计str在target中出现的个数
    int match(char *target,int n,char * str,int m){
        int k=0,cnt=0;
        for(int i=0;i<n;i++){
            while(k>0 && str[k]!=target[i])
                k=next[k];
            if(str[k]==target[i])
                k++;
            if(k==m){
                cnt++;
                k=next[k];
            }
        }
        return cnt;
    }
    
    //n为target字符串的长度,m为str字符串的长度
    //若存在str字符串,返回匹配成功的第一个字符的位置
    int match(char *target,int n,char * str,int m){
        int k=0,cnt=0;
        for(int i=0;i<n;i++){
            while(k>0 && str[k]!=target[i])
                k=next[k];
            if(str[k]==target[i])
                k++;
            if(k==m){
                return i-m+1;
            }
        }
        return -1;
    }
    View Code

    某大神的模板(其实和算法导论一样):

    #define KMP_GO(X) while(k>0 && P[k]!=X[i]) k=next[k];if(P[k]==X[i])k++
    //求字符串P在T中出现的次数
    int kmp_match(char*T,char*P){
        int n,m,next[10010],i,k,c;
        n=strlen(T);m=strlen(P);
        next[1]=k=0;
        for(i=1;i<m;i++){
            KMP_GO(P);
            next[i+1]=k;//这里i表示的是字符的索引,对应的长度i+1
        }
        k=c=0;
        for(i=0;i<n;i++){
            KMP_GO(T);
            if(k==m){
                c++;
                k=next[k];
            }
        }
        return c;
    }
    View Code

     

    二:KMP最小循环节、循环周期:

    定理:假设S的长度为len,则S存在最小循环节,循环节的长度L为len-next[len],子串为S[0…len-next[len]-1]。

    (1)如果len可以被len - next[len]整除,则表明字符串S可以完全由循环节循环组成,循环周期T=len/L。

    (2)如果不能,说明还需要再添加几个字母才能补全。需要补的个数是循环个数L-len%L=L-(len-L)%L=L-next[len]%L,L=len-next[len]。

     

     

    理解该定理,首先要理解next数组的含义:next[i]表示前面长度为i的子串中,前缀和后缀相等的最大长度。

    如:abcdabc

    index

    0

    1

    2

    3

    4

    5

    6

    7

    char

    a

    b

    c

    d

    a

    b

    C

     

    next

    -1

    0

    0

    0

    0

    1

    2

    3

     

    如对于a,ab,abc,abcd,很明显,前缀和后缀相同的长度为0

      对于长度为5的子串abcda,前缀的a和后缀的a相同,长度为1

      对于长度为6的子串abcdab,前缀的ab和后缀的ab相同,长度为2

    接下来举几个例子来说明最小循环节和循环周期:

    为方便说明,先设字符串的长度为len,循环子串的长度为L

    1.

    s0s1s2s3s4s5 ,next[6]=3

    即s0s1s2=s3s4s5

    很明显可知:循环子串为s0s1s2,L=len-next[6]=3,且能被len整除。

     

    2.

    s0s1s2s3s4s5s6s7 ,next[8]=6

    此时len-next[8]=2 ,即L=2

    由s0s1s2s3s4s5=s2s3s4s5s6s7

    可知s0s1=s2s3,s2s3=s4s5,s4s5=s6s7

    显然s0s1为循环子串

     

    3.

    s0s1s2s3s4s5s6 ,next[7]=4

    此时len-next[7]=3,即L=3

    由s0s1s2s3=s3s4s5s6

    可知s0s1=s3s4,s2s3=s5s6

    从而可知s0s1s2=s3s4s5,s0=s3=s6

    即如果再添加3-4%3=2个字母(s1s2),那么得到的字符串就可以由s0s1s2循环3次组成

     

    这个定理可以这么理解:

    http://www.cnblogs.com/oyking/p/3536817.html

    对于一个字符串,如abcd abcd abcd,由长度为4的字符串abcd重复3次得到,那么必然有原字符串的前八位等于后八位。

    也就是说,对于某个字符串S,长度为len,由长度为L的字符串s重复R次得到,当R≥2时必然有S[0..len-L-1]=S[L..len-1],字符串下标从0开始

    那么对于KMP算法来说,就有next[len]=len-L。此时L肯定已经是最小的了(因为next的值是前缀和后缀相等的最大长度,即len-L是最大的,那么在len已经确定的情况下,L是最小的)。

     

    如果一定仔细证明的话,请看下面:

    (参考来自:http://www.cnblogs.com/wuyiqi/archive/2012/01/06/2314078.html,有所改动)

     k    m   x     j     i

    由上,next【i】=j,两段红色的字符串相等(两个字符串完全相等),s[k....j]==s[m....i]

    设s[x...j]=s[j....i](xj=ji)

    则可得,以下简写字符串表达方式

    kj=kx+xj;

    mi=mj+ji;

    因为xj=ji,所以kx=mj,如下图所示

     k   m     a    x    j     i

    设s[a…x]=s[x..j](ax=xj)

    又由xj=ji,可知ax=xj=ji

    即s[a…i]是由s[a…x]循环3次得来的。

    而且看到没,此时又重复上述的模型,s[k…x]=s[m…j],可以一直递推下去

    最后可以就可以递推出文章开头所说的定理了。

     

    最后再举两个相关例子

    abdabdab  len:8 next[8]:5

    最小循环节长度:3(即abd)   需要补的个数是1  d

    ababa  len:5 next[5]:3

    最小循环节长度:2(即ab)    需要补的个数是1  b

    一道求循环节的题目

    题目链接:https://oj.ismdeep.com/contest/Problem?id=1284&pid=6

    G: Dave的时空迷阵

    Time Limit: 1 s      Memory Limit: 128 MB     

    Problem Description

    皇家理工本部隐藏着一座扭曲时空的迷阵,一旦陷入迷阵就不能复出。Dave作为一个勇敢的探险家,勇敢闯入迷阵,并发现了一些规律……

    Dave发现总是在行进一定距离后回到起点,继续走上重复的路途….

    冷静分析之后,Dave在前进的路途中记录了标记(a-z的小写字母),并得到了一个字符串,Dave想知道,从起点开始,最少走多远会回到初始状态?

    Input

    第一行一个正整数nn为Dave记录的字符串长度(1n2×105)(1≤n≤2×105)

    第二行为长度nn的字符串,仅包含aza−z的小写英文字母的非空字符串

    Output

    第一行输出从起点再到起点的距离

    第二行输出行进路上遇到的字符

    Sample Input

    4
    abcd
    

    Sample Output

    4
    abcd

    解题思路:对于给定串求出 next 数组,利用循环节性质得出是否满足循环,再将末尾多余的字母隔过 去,输出一个整的循环节即可


    附上代码
    #include <bits/stdc++.h>
    using namespace std;
    const int AX = 3e5+666;  
    char ps[AX];
    int len ;
    int next1[AX];
    void getnext1( int m )
    {
        next1[0] = -1;
        int j = 0;
        int k = -1;
        while ( j < m ) 
        {
            if (k == -1 || ps[j] == ps[k])
                next1[++j] = ++k;
            else
                k = next1[k];
        }
    }
    int main()
    {
        scanf("%d",&len);
        scanf("%s",ps);
        getnext1(len);
    
        int ans = len - next1[len];
        int left = next1[len] % ans ;
        printf("%d
    ",ans);
        for( int i = len - left - ans ; i < len - left ; i ++ )
            printf("%c",ps[i]);
        printf("
    ");
        return 0;
    }
  • 相关阅读:
    Apache配置虚拟主机的三种方法(基于IP、端口、域名)
    shell中嵌套执行expect命令实例(利用expect实现自动登录)
    Shell脚本实现SSH免密登录及批量配置管理
    搭建本地yum源服务器
    awk之腾迅面试题1
    16个tomcat面试题
    tomcat常见面试题1
    Mysql经典面试题
    10个超有趣的linux命令
    Codeforces Beta Round #79 (Div. 2 Only)
  • 原文地址:https://www.cnblogs.com/zjl192628928/p/9552949.html
Copyright © 2011-2022 走看看