算法日志(3)------------KMP

KMP算法

(有基础的从1.2看起)

0.1 匹配的烦恼

Y同学是一个热爱编程的人,在1985年时获得NOI金牌,进入了国家集训队,他在做题时发现了一个问题:

给你一个长度有100W的字符串S,再给你一个子串TT属于S),求TS中的位置。

Y不是等闲之辈,刷刷刷打了一串匹配算法,将TS逐位比较,直到匹配成功为止。小Y的测试如下图

SABDCAABBCABD

TCAB

当小Y提交时,返回(时间超限100%),小Y测算了时间复杂度,将近能跑一天!!!

这可把小Y难住了。

直到1987年时,三位IOI大佬D.E.KnuthJ.H.MorrisV.R.Pratt同时发现了一种匹配算法解决了世纪难题,于是为了纪念这三个人,我们把这种匹配算法叫做

KD.E.KnuthMJ.H.MorrisPV.R.Pratt)算法。

1.1 优化吧!!!

我们不难发现,在小Y的算法中有许多不必要的匹配:

第一次:

SABDCAABBCABD

TCAB

失配!

第二次:

SABDCAABBCABD

TCAB

失配!

第三次:

SABDCAABBCABD

T:  CAB

失配!

......

10

SABDCAABBCABD

T:         CAB

匹配成功!!

显然!!!有一些匹配是显而易见不用匹配的,在这组数据中看不出来什么问题,样例再加5个零,如果在100%数据中On方的算法可以让你炸飞天得到0分。

所以KMP的精髓所在就是优化这些不必要的匹配,从而达到优化的效果。

1.2 HashKMP的对决(1

有一些编程大佬肯定会说:我会哈希(假的)还有哈希,别这么肯定!!!

我承认,hash确实可以,但是我要知道你的hash magic呢???那么我就可以出个刚好让你hash magic重叠的数据,让你GG

对于字符串的匹配,KMP的出现让hash的威力下降一半。

KMP的精髓就是通过公共的前缀与后缀达到NEXT数组转移从而降低时间复杂度的效果。

什么是前缀?

答:对于字符串S,除去最后一位,S[1..n-1]中的所有以S[1]为起点的子串(不为空)叫做S的前缀。

后缀同理。

这里有一点要注意,前缀必须要从头开始算,后缀要从最后一个数开始算,中间截一段相同字符串是不行的。

1.3  Next数组

我们定义Next[i]代表了前缀与后缀长度为i时,前缀与后缀最长重复子串的个数。

举个例子:

对于字符串st = "ababaaababaa";

next[1] = -1,代表着除了第一个元素,之前前缀后缀最长的重复子串,这里是空 ,即"",没有,我们记为-1,代表空。(0代表1位相同,1代表两位相同,依次累加,当然,你也可以记为0,随个人喜好)。

next[2] = -1,即“a”,没有前缀与后缀,故最长重复的子串是空,值为-1;

next[3] = -1,即“ab”,前缀是“a”,后缀是“b”,最长重复的子串为空

next[4] = 1,即"aba",前缀是“ab”,后缀是“ba”,最长重复的子串“a”;

next[5] = 2,即"abab",前缀是“aba”,后缀是“bab”,最长重复的子串“ab”;

next[6] = 3,即"ababa",前缀是“abab”,后缀是“baba”,最长重复的子串“aba”;

next[7] = 1,即"ababaa",前缀是“ababa”,后缀是“babaa”,最长重复的子串“a”;

next[8] = 1,即"ababaaa",前缀是“ababaa”,后缀是“babaaa”,最长重复的子串“a”;

next[9] = 2,即"ababaaab",前缀是“ababaaa”,后缀是“babaaab”,最长重复的子串“ab”;

next[10] = 3,即"ababaaaba",前缀是“ababaaab”,后缀是“babaaaba”,最长重复的子串“aba”;

next[11] = 4,即"ababaaabab",前缀是“ababaaaba”,后缀是“babaaabab”,最长重复的子串“abab”;

next[12] = 5,即"ababaaababa",前缀是“ababaaabab”,后缀是“babaaaababa”,最长重复的子串“ababa”;

也就是说,Next中的数字就是遇到这一位可以跳过的位数,前缀与后缀有公共部分,不需要每次比较,因为肯定失配在这一位,直接跳过,知道死在新的地方或匹配成功。

这样可以省下许多不必要的匹配。

Y送给大家的代码:

#include

char str[10000];

int next[10000];

void Get_next(char *str, int *next, int len)

{

next[0] = -1;//next[0]初始化为-1-1表示不存在相同的最大前缀和最大后缀

//可以是Next[0]=0;

int k = -1;

    for (int q = 1; q <= len-1; q++)

    {

        while (k >-1&& str[k + 1] != str[q])//如果下一个不同,那么k就变成next[k],注意next[k]是小于k的,无论k取任何值。当然,能力好的同学改成       while (k&&str[k+1]!=str[q])OK

        {

            k = next[k];//往前回溯

        }

        if (str[k + 1] == str[q])//如果相同,k++

        {

            k++;

        }

        next[q] = k;//这个是把算的k的值(就是相同的最大前缀和最大后缀长)赋给next[q]

    }

}

int main(){

scanf("%s",str);

int len=strlen(str);

Get_next(str,next,len);

}

 

2.1 KMP算法

如果大家搞懂了Next数组,恭喜你!KMP主算法是与Next十分相近的,也就是说,Next数组是SS的自我比较,求出相同的最大前缀和最大后缀长,而KMP主算法就是ST的比较与失配,代码与Next数组十分相近的,举个栗子:

对于S=ABDCAABBCABD”

对于T=ABC

大家可以先用学过的知识判断出next数组。

答:next=-1 -1 -1 0 0 1 -1 -1 0 1 2 0

ABDCAABBCABD

ABC

失配在第二位,因为没有Next前科的比较,老老实实匹配。

ABDCAABBCABD

 ABC

失配在第一位,没有前科,下一位

ABDCAABBCABD

  ABC

没有前科,下一位。

ABDCAABBCABD

ABC

下一。。。等一下,Next[4]=0,也就是说,Next已经预测到下一位失配的地方2,直接跳过下一次!!!

ABDCAABBCABD

      ABC

Next[6]=1,跳过两位!!!

ABDCAABBCABD

         ABC

Next[9]=0,越过一位!

ABDCAABBCABD

           ABC

T串已经出界,无需继续比较,只需要判断当前Ttail是否大于S串就OK了。

KMP只需要比较7次就可以完成匹配,发现无解,而暴力算法需要10次才能判断无解,是不是很省时???

Y的代码:

int KMP(char *str, int slen, char *ptr, int plen)

{

    int *next = new int[plen];

    Get_next(ptr, next, plen);//计算next数组

    int k = -1;

    for (int i = 0; i < slen; i++)

    {

        while (k >-1&& ptr[k + 1] != str[i])//ptrstr不匹配,且k>-1(表示ptrstr有部分匹配)

            k = next[k];//往前回溯

        if (ptr[k + 1] == str[i])

            k = k + 1;

        if (k == plen-1)//说明k移动到ptr的最末端

        {

            return i-plen+1;//返回相应的位置

        }

    }

    return -1;  

}

 

2.2 时间复杂度

相当于暴力的O|S|*|T|)的时间复杂度,KMP肯定是要快得多,但它的时间复杂度不是太好求,我们把KMP分成两部分,一部分是Get_Next,另一部分是KMP主算法,Get_next的复杂度是相对于while说的,想要知道while的复杂度,必须先知道K的规律,题意已知k是绝对<=lenS,也就是说,K++最多会执行lenS次,同时k=next[k]也会执行LenS次,也就是说,对于while循环的均摊复杂度为θ1),那么整个Get_next的时间复杂度为O(|S|)。

对于KMP主算法来说,跟Get_next一样,k只会执行LenS次,但是,这一次只要找到就返回,这让均摊复杂度大大降低,会降低到θ (|T|),将这两部分合并起来,我们就知道了KMP的时间复杂度O(|S|+|T|)。

也就是说KMP与暴力不是同一个层次的,暴力是二维的复杂度,KMP只有一个维度,相当于100W的数据,完全是OK的。

Y证明出来时间复杂度后,十分高兴,发表了一篇论文在微博上。。。

3.1 HashKMP的对决(2

Hash看上去只有O|s|)的时间复杂度,但是是一种相当不稳定的算法,当你的算法透明化后,我可以让你Hash全发生碰撞,在NOIIOI上是不太保险的算法,如你的Hash magic666233,则我可以出一个666233进制数,让你发生碰撞,这就是虽然看上去不太优的KMP算法,但是依然存在的原因,这也是KMP存在的意义:一种比Hash更稳定的算法。但我不否认Hash+map依然可以解题

----------------------------------------------------------------------------------------------------------------

彩蛋:当然,KMP算法在下几章中将会有大大升级,从而达到刷更坑在树上的字符串匹配的题目,这是Hash力所不能及的,这将是一款秘密武器(大家可以在评论里猜或回答这是神马算法)。

3.2 例题讲解

Y为了让自己提高,找到了一些题目。

统计数量のKMPkmp

题目描述

给你两个字符串ps,求出ps中出现的次数

输入

第一行字符串p

第二行字符串s

字符串长度小于等于1000000

输出

一个整数表示答案

样例输入

ab

ababab

样例输出

3

 

提示:想想要不要在找到后直接结束,不结束又跳到哪里。

Y的代码(仅供参考 C++):

#include

#include

using namespace std;

 

const int MAXW=1000001,MAXT=1000001;

char W[MAXW],T[MAXT];

int next[MAXW];

int lenW,lenT;

 

void getnext(int lenW)

{

    int i=0,j=-1;

    next[i]=-1;

    while(i

        if(j==-1||W[i]==W[j]) {

            next[++i]=++j;

        }

        else j=next[j];

    }

}

 

int kmp(int pos,int lenW,int lenT)

{

    int i=pos,j=0,ans=0;

    while(i

        if(T[i]==W[j]||j==-1) ++i,++j;

        else j=next[j];

        if(j==lenW) {

            ans++;

            j=next[j-1];

            i--;

        }

    }

    return ans;

}

 

 

int main()

{

 

        scanf("%s",W);

        scanf("%s",T);

        lenW=strlen(W);

        lenT=strlen(T);

        getnext(lenW);

        printf("%d\n",kmp(0,lenW,lenT));

 

    return 0;

}

思考题:统计数量のKMP2kmp2

题目描述

给你字符串ss是由另一个字串A复制而成,如S=”ABABAB”,A就等于AB

A的最大长度。

输入

第一行字符串s

字符串长度小于等于1000000

输出

一个整数表示答案

样例输入

ababab

样例输出

2

提示:next数组有什么性质。

题解:先求Next[|S|],如果|S|-Next[|S|]能被|S|整除,则输出|S|-Next[|S|],否则输出|S|

                

 

 

 

 

 

 

你可能感兴趣的:(算法日志,KMP)