KMP算法
(有基础的从1.2看起)
0.1 匹配的烦恼
小Y同学是一个热爱编程的人,在1985年时获得NOI金牌,进入了国家集训队,他在做题时发现了一个问题:
给你一个长度有100W的字符串S,再给你一个子串T(T属于S),求T在S中的位置。
小Y不是等闲之辈,刷刷刷打了一串匹配算法,将T与S逐位比较,直到匹配成功为止。小Y的测试如下图
S:ABDCAABBCABD
T:CAB
当小Y提交时,返回(时间超限100%),小Y测算了时间复杂度,将近能跑一天!!!
这可把小Y难住了。
直到1987年时,三位IOI大佬D.E.Knuth、J.H.Morris和V.R.Pratt同时发现了一种匹配算法解决了世纪难题,于是为了纪念这三个人,我们把这种匹配算法叫做
K(D.E.Knuth)M(J.H.Morris)P(V.R.Pratt)算法。
1.1 优化吧!!!
我们不难发现,在小Y的算法中有许多不必要的匹配:
第一次:
S:ABDCAABBCABD
T:CAB
失配!
第二次:
S:ABDCAABBCABD
T: CAB
失配!
第三次:
S:ABDCAABBCABD
T: CAB
失配!
......
第10次
S:ABDCAABBCABD
T: CAB
匹配成功!!
显然!!!有一些匹配是显而易见不用匹配的,在这组数据中看不出来什么问题,样例再加5个零,如果在100%数据中On方的算法可以让你炸飞天得到0分。
所以KMP的精髓所在就是优化这些不必要的匹配,从而达到优化的效果。
1.2 Hash与KMP的对决(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数组是S与S的自我比较,求出相同的最大前缀和最大后缀长,而KMP主算法就是S与T的比较与失配,代码与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])//ptr和str不匹配,且k>-1(表示ptr和str有部分匹配)
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 Hash与KMP的对决(2)
Hash看上去只有O(|s|)的时间复杂度,但是是一种相当不稳定的算法,当你的算法透明化后,我可以让你Hash全发生碰撞,在NOI、IOI上是不太保险的算法,如你的Hash magic为666233,则我可以出一个666233进制数,让你发生碰撞,这就是虽然看上去不太优的KMP算法,但是依然存在的原因,这也是KMP存在的意义:一种比Hash更稳定的算法。但我不否认Hash+map依然可以解题
----------------------------------------------------------------------------------------------------------------
彩蛋:当然,KMP算法在下几章中将会有大大升级,从而达到刷更坑在树上的字符串匹配的题目,这是Hash力所不能及的,这将是一款秘密武器(大家可以在评论里猜或回答这是神马算法)。
3.2 例题讲解
小Y为了让自己提高,找到了一些题目。
统计数量のKMP(kmp)
题目描述
给你两个字符串p和s,求出p在s中出现的次数
输入
第一行字符串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;
}
思考题:统计数量のKMP2(kmp2)
题目描述
给你字符串s,s是由另一个字串A复制而成,如S=”ABABAB”,A就等于AB,
求A的最大长度。
输入
第一行字符串s
字符串长度小于等于1000000
输出
一个整数表示答案
样例输入
ababab
样例输出
2
提示:next数组有什么性质。
题解:先求Next[|S|],如果|S|-Next[|S|]能被|S|整除,则输出|S|-Next[|S|],否则输出|S|。