KMP算法

KMP

kmp算法:以O(N+M)的效率完成单个子字符串在主字符串中的匹配

对比一下朴素暴力和kmp,思考一下为什么kmp能获得更稳定更小的时间复杂度

举个例子,在

0000000000000000000000000000000001(n-1个0)

匹配

000001(m-1个0)

如果按朴素的暴力算法,那么在前面n-m个0中匹配时,都必须花费m-1的复杂度去匹配前面(m-1)个0,才能跳到下一位开始匹配,那复杂度约等于

O(NM)

当然,如果是

11111111111111111111111111111111111111111111111

000001

这种情况我们会发现复杂度约等于

O(N)

虽然被卡到O(NM)的概率不大,但是这也是我们不能接受的(参见NOI2017 DAY1 T1 归程,我现在已经放弃SPFA写堆优化DIJ了)

KMP算法在这种暴力算法最劣的情况也能保持一个优秀的复杂度(常数大一丢丢的N+M),在理想情况下和正常暴力复杂度没有明显优势,但这也已经能看出KMP稳定性的优点。

为什么普通暴力会在最劣情况下效率如此之低?

回到前文,在

0000000000000000000000000000000001(n-1个0)

匹配

000001(m-1个0)

明明每次已经匹配了m-1个0,但是暴力算法在下一位仍然保持很蠢的从第一位开始匹配。

假想一下,如果以我们人类的思维去做这个匹配,(一眼看出结果是因为你已经看完这个主串了),你拿张纸盖住主串,一位一位向右看,你数了m-1个0,下一位你发现是仍然是0不是1,你肯定会直接往后数,因为你知道前面m-1个0肯定是匹配的。

那么我们可以想到,应该让程序记录已经匹配的一部分字符串以减少这种无谓的时间开销。

具体思路,我们直接看KMP算法。

首先补充一些概念:以字符串000001为例

第一个字串 0 前后缀都不存在 记为next[1] = 0

第二个字串 00 前后缀都是0 长度为1 记为next[2] = 1

第三个字串 000 前后缀都是00 长度为2 记为next[3] = 2

第四个字串 0000 前后缀都是000 长度为3 记为next[4] = 3

第五个字串 00000 前后缀都是0000 长度为4 记为next[5] = 4

第六个字串 000001 前缀不带1后缀带1 不存在公共前后缀 记为next[6] = 0

这个next数组就是最长公共前后缀的长度,他有什么用呢?

前缀表决定了子串指针在匹配失败时回溯到的位置

回到暴力匹配的例子

0000000000000000000000000000000001(n-1个0)

匹配

000001(m-1个0)

一开始匹配到00000后,出现1和0匹配失败的现象

此时遍历主串的下标i = 5不动,遍历子串的下标j= 6先回退到能匹配的上一位j - 1 = 5

令j = next[j] = 4

此时匹配

  • 情况1:只要比较字串第5位与主串当前位相同,就可以继续遍历主串,节省了比较前面几位的匹配。

  • 情况2:如果还是不相同,那就继续比较,令j = next[j]知道出现情况1

不会出现非常劣的复杂度(即n次的情况2)因为出现在第j位失配的前提是j-1位可以匹配,可以思考一下。

然后是另一个问题,关于求next数组,如果用m^2的方法未免有点呆,可以观察一下,通过类似递推的方式获取next数组。

如果已有
n e x t [ i − 1 ] = k next[i - 1] = k next[i1]=k
即str[1] -> str[k] == str[i - k] - > str[i - 1]

那么

  • 若str[k + 1] == str[i]
    n e x t [ i ] = k + 1 next[i] = k + 1 next[i]=k+1

  • 若str[k + 1] != str[i]

    可以发现这是类似于KMP算法中失配的情况,事实上我们可以在这里就运用这种思想,令 k = next[k - 1]

贴个代码

int next[Maxm];
void get_Next(string s)
{
	int j = 0;
	next[0] = 0;	
	for(int i = 1; i<s.size(); i++){	
	//i指针指向的是后缀末尾,j指针指向的是前缀末尾
		while(j>0&&s[i]!=s[j])	j = next[j-1];	
		if(s[i]==s[j])	j++;	
		next[i] = j;	
	}
}

那么类似的也就有KMP了(洛谷模板题code)

#include <cstdio>
#include <iostream>
#include <algorithm>
#define Maxm 1000001
using namespace std;
int nxt[Maxm];
void get_Next(string s){
    int j = 0;
    nxt[0] = 0;
    for(int i = 1;i < s.size();i ++){
        while(j && s[i] != s[j]) j = nxt[j - 1];
        if(s[i] == s[j]) j ++;
        nxt[i] = j;
    }
}
void Kmp(string s,string t){ // s中匹配t
    int ans = 0;
    if(!t.size()) return;
    get_Next(t);
    int j = 0;
    for(int i = 0;i < s.size();i ++){
        while(j && s[i] != t[j]) j= nxt[j - 1];
        if(s[i] == t[j]) j ++;
        if(j == t.size()) {cout << i + 2 - t.size()<< endl;j = nxt[j - 1];}
    }
}
int main(){
    string a,b;
    cin >> a >> b;
    Kmp(a,b);
    for(int i = 0;i < b.size();i ++)
        cout << nxt[i] << " ";
}
  • 40
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值