• 【字符串】KMP


    【匹配字符串S与T,判断T是否为S的子串】

     

    即在主串S中快速匹配是否存在一个子串等同于模式串T

     

    S为被匹配串(主串),T为匹配串(模式串)

     

    实现方式:

     

    在最普通的算法中,我们总是拿两个指针指向两个字符串的不同位置来匹配

     

     

     

    如果某个字符匹配成功就把两个光标同时移动到下一个位置,即

     

     

     

    如果不匹配,那么模式串就该整体右移一位,重复上述方式进行匹配

    模式串的光标要返回模式串的第一个位置,主串的光标移动到移动后模式串开头对应的主串的位置,即

     

     

     

    然后再继续匹配下去,直到模式串中的光标到达模式串的末尾,且最后一个字符也能匹配时(说明在原串中找到了一个子串等同于模式串),此时就能作为答案输出

     

     

     

    但是,每一次不匹配就必须回溯光标到模式串开头对应,然后模式串整体往右移动一位,再继续匹配下去

     

    为了能够做到O(n)时间内完成匹配,可以从这两点入手,去优化这一匹配的过程

     

    引入Next数组概念,Next数组是从模式串中获得的

     

    Next[i] 表示现在模式串已经匹配到了第 i-1 位,但是第 i 位的字符与原串不匹配

     

     

     

    可以发现已匹配的串abcab中,前缀ab等于后缀ab

    所以抓住这个特征,下一步就可以直接让模式串右移到后缀位置

     

     

     

    可以发现移动后后缀位置是已经完成匹配的,所以原串的光标位置不变,继续匹配下去(O(n)的原因)

     

    如果这样还是不能匹配,继续抓住已匹配的串ab继续右移下去

    发现ab没有前缀等于后缀的情况出现(前缀后缀不能等于原串)

     

    说明模式串只能直接移动到此时不匹配的位置上继续下去,即

     

    所以求出Next数组是关键

    void getNext(){
        int i,j=-1;
        Next[0]=-1;
        for(i=1;i<Tlen;i++){
            while(j>-1 && T[ j+1 ]!=T[ i ])
                j=Next[j];
            if(T[j+1]==T[i])
                j++;
            Next[i]=j;
        }
    }

    Next数组就是寻找前 i 个字符 前缀=后缀 的最长长度,且前缀=后缀≠原子串

    在执行过程中,有以下几个例子://下标从0开始

        对于aba,在查找ab时可以得知Next[1]=-1,此时j=-1,所以i=2时对比的是j+1=0,发现a=a成立,所以j=0,Next[2]=0

           再比如aaaaa,在查找aaaa时得知Next[3]=2 , j=2 所以i=4时对比的是j+1=3,发现a=a成立,j++,Next[4]=j=3

           再比如aaaab,在查找aaaa时得知Next[3]=2 , j=2 所以i=4时对比的是j+1=3,发现a=b不成立,j=Next[j]=Next[3]=2,对比i=4,j=2不成立,继续,直到j=-1,所以最后Next[4]=-1

     

    然后到应用部分

    int KMP_Position(){
        int i,j=-1;
        for(i=0;i<Slen;i++){
            while(j>-1&&T[j+1]!=S[i])
                j=Next[j];
            if(T[j+1]==S[i])
                j++;
            if(j==Tlen-1)
                return i-Tlen+1;//找到后返回位置
        }
        return -1;//没找到
    }

     

    其中,对于下面这一句

    while(j>-1&&T[j+1]!=S[i])
        j=Next[j];

    这句说明在第 i 个位置原串S与模式串T失配了,但是由上面可以得知我们不需要回溯原串的光标i,只需要回溯模式串光标 j 即可,而j=Next[j] 就是指应该回溯到哪个位置(指模式串应该右移到什么位置),然后继续匹配此时的原串 i 位置和模式串 j 位置,直到完全不匹配时(j = -1)或者找到匹配位置(T[j+1] != S[i] )时退出这个循环

     

    if(T[j+1]==S[i])
        j++;

    这句判断跳出while循环的条件是不是由匹配才退出的,如果是匹配的话,j可以+1

     

    然后记录此时Next[i] 的值

     

    如果j==Tlen-1 成立,说明模式串的光标已经到了模式串的末尾,且最后一个位置也是匹配的,所以此时就能返回答案位置了—— i -Tlen + 1

     

    另外,KMP还能快速查找模式串在原串中出现的次数,只要把输出条件更改成

    if(j==Tlen-1){
        cnt++;
        j=Next[j];
    }

     

    然后用cnt作为答案即可

     

     

    完整程序:

    #include<bits/stdc++.h>
    using namespace std;
    
    string S,T;
    int Slen,Tlen,Next[1000050];
    
    void getNext(){
        int i,j=-1;
        Next[0]=-1;
        for(i=1;i<Tlen;i++){
            while(j>-1&&T[j+1]!=T[i])
                j=Next[j];
            if(T[j+1]==T[i])
                j++;
            Next[i]=j;
        }
    }//模式串T的Next数组预处理
    
    int KMP_Position(){
        int i,j=-1;
        for(i=0;i<Slen;i++){
            while(j>-1&&T[j+1]!=S[i])
                j=Next[j];
            if(T[j+1]==S[i])
                j++;
            if(j==Tlen-1)
                return i-Tlen+1;
        }
        return -1;
    }//匹配模式串第一次出现在主串中的位置
    
    int KMP_Count(){
        int i,j=-1,cnt=0;
        for(i=0;i<Slen;i++){
            while(j>-1&&T[j+1]!=S[i])
                j=Next[j];
            if(T[j+1]==S[i])
                j++;
            if(j==Tlen-1){
                cnt++;
                j=Next[j];
            }
        }
        return cnt;
    }//匹配模式串在主串中出现的次数
    
    int main(){
        ios::sync_with_stdio(0);
        cin.tie(0);cout.tie(0);
        cin>>S>>T;
        Slen=S.size();
        Tlen=T.size();
        getNext();
        //cout<<KMP_Position()<<'
    ';
        //cout<<KMP_Count()<<'
    ';
        
        return 0;
    }

     

    附:

    因为KMP算法的 getNext 函数求的是T字符串的前 i 个字符最长的 前缀=后缀 的长度,且这个长度不等于自身长度

    所以如果求出的 Next[ LEN ] 满足

    LEN % ( LEN - Next[ LEN] ) == 0

    就可以说明这个字符串是由某个子串循环 LEN / ( LEN - Next[ LEN] )  次得到的

    且这个子串是最短循环子串

    也就是说, LEN / ( LEN - Next[ LEN] )  是字符串子串中最多的循环次数

    例题1 POJ 2406

    题目求的就是某个子串在整个字符串中循环的最多次数

    例题2 POJ 1961

    求的是所有循环次数大于等于 2 的循环节的循环次数

  • 相关阅读:
    [转]android Intent机制详解
    [转]Android进程与线程基本知识
    HTML背景图片自适应
    边框边界填充理解
    [转]Android 代码自动提示功能
    [转]Windows7:Visual Studio 2008试用版的评估期已经结束解决方法
    eclipse安装、汉化、搭建安卓开发环境
    asp.net控件拖不动。控件错误
    opengl 入门浅学(一)
    opengl 无法定位程序输入点_glutInitWithExit于动态链接库glut32.dll上
  • 原文地址:https://www.cnblogs.com/stelayuri/p/12506196.html
Copyright © 2020-2023  润新知