KMP算法

KMP算法

字符串匹配问题

​ 字符串匹配是一种计算机会频繁使用的算法。,例如有一个字符串主串S:knock the heaven's door,现在需要知道S中是否包含子串P:heaven。这是一个十分常见的问题,由于使用次数很多,所以算法的效率是十分重要的。

朴素算法

​ 首先来讲,最朴素的方法莫过于是顺次比较,假定主串S的长度为n,子串P的长度是m,我们依次从主串的每一个位置开始和子串进行比较,如果匹配失败了则换到主串的下一个位置开始,该方法的时间复杂度为O(mn)。

简介

​ 朴素算法效率不高的原因在于,每次匹配失败后都仅向后移动一位,如果在匹配失败后根据某些信息多移动几位再进行比较,速度肯定会更快,而KMP算法就是提前预处理好一些信息,当我们匹配失败后移动不止一个位置,从而提升算法效率。

​ KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP算法)。KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。

思想

​ 对于上面的朴素做法,我们发现第三次比较不必进行,直接比较第四步,很明显这是一种改进,如果能进行多次这样的改进,程序运行的速度肯定会有有所提升。

​ 实际上我们发现改进的方法是找到一个准确的位置,发生失配情况时,将模式串尽可能地往右移动。这个位置和什么有关呢,该如何确定?来看下面的匹配情况:

​ 其中S表示主串,P表示子串,在第5个字符发生失配的时候,我们尝试尽可能的向后移动,不难发现向后移动2个位置是最佳的位置,从S的第三个位置开始进行比较,这样就完成了匹配,为什么可以向后移动2个位置呢,其实这和P串本身有关,很容易发现,P串的前4个字符ABAB,是两两匹配的,也就是说如果P串在第5个字符匹配失败的时候,我们可以直接将移动两个位置,因为这两个位置的字符和P的开头两个字符相同,同时对应到S串上也必定一样。

进一步推广,也就是说当在i处匹配失败时,i要移动的距离j。存在着这样的性质:最前面的j个字符和i之前的最后j个字符是一样的(假定从位置1开始存储),即 p[1 ~ j] = p[i-j+1 ~ i]。其中j的含义实际上就是最长公共前后缀,并且j的值与主串S无关,只和模式串P本身有关。

做法

​ 有了上面的思路,我们只需要计算出模式串P的每一个位置的最长公共前后缀即可,通常用数组next来进行记录,对于next[j],表示的含义是:p[1, j]中最长公共的前后缀,即 p [ 1 , n e x t [ j ] ] = p [ j − n e x t [ j ] + 1 , j ] p[1, next[j]] = p[j-next[j]+1,j] p[1,next[j]]=p[jnext[j]+1,j]

例如对于下面的P串,它的next数组值应该为:

Pabcab
下标12345
next值00012

​ 如何求next数组呢?实际上next数组是通过自己和自己比较得出的。

​ 朴素做法是去枚举这样的 j ∈ [ 1 , i − 1 ] j \in [1, i-1] j[1,i1],并且满足条件 A [ i − j + 1 , i ] = A [ 1 , j ] A[i - j +1, i] = A[1,j] A[ij+1,i]=A[1,j],找出 j j j的最大值,这个做法对于每一个 i i i都要枚举 i − 1 i-1 i1次才能找出最大值,时间复杂度大致为 O ( N 2 ) O(N^2) O(N2)

根据next数组的定义,可以初始化next[1] = j = 0,假定我们已经计算出了next[i - 1]的值,现在要计算next[i]的值,其中j表示匹配的长度,如果下一位匹配成功,即 a [ i ] = = a [ j + 1 ] a[i] == a[j+1] a[i]==a[j+1],那么匹配长度增加1位(j ++),同时next[i] = j,如果下一位没有匹配成功,即 a [ i ] ≠ a [ j + 1 ] a[i] \ne a[j+1] a[i]=a[j+1]我们只需让j = next[j],重复这个过程,直至匹配成功为止。

原理:(以下部分来自于《算法竞赛进阶指南》一书)

引理:若 j 0 j_0 j0 n e x t [ i ] next[i] next[i]的一个 “候选项” ,即 j 0 < i j_0 < i j0<i A [ i − j 0 + 1 , i ] = A [ 1 , j 0 ] A[i - j_0 + 1 , i] = A[1,j_0] A[ij0+1,i]=A[1,j0],则小于 j 0 j_0 j0的最大的 n e x t [ i ] next[i] next[i]的 “候选项”是 n e x t [ j 0 ] next[j_0] next[j0]。换言之, n e x t [ j 0 ] + 1 next[j_0] + 1 next[j0]+1 ~ j 0 − 1 j_0-1 j01之间的数都不是 n e x t [ i ] next[i] next[i]的 “候选项。”

那么根据引理,当 n e x t [ i − 1 ] next[i - 1] next[i1]计算完毕时,我们可得知, n e x t [ i − 1 ] next[i -1] next[i1] 的所有 “候选项” 从大到小依次是 n e x t [ i − 1 ] , n e x t [ n e x t [ i − 1 ] ] , n e x t [ n e x t [ n e x [ i − 1 ] ] ] . . . next[i-1], next[next[i-1]],next[next[nex[i-1]]]... next[i1],next[next[i1]],next[next[nex[i1]]]...,而如果一个整数 j j j n e x t [ i ] next[i] next[i]的候选项,那么 j − 1 j-1 j1显然也必须是 n e x t [ i − 1 ] next[i-1] next[i1]的 “候选项”。因此在计算 n e x t [ i ] next[i] next[i]时,只需要把 n e x t [ i − 1 ] + 1 , n e x t [ n e x t [ i − 1 ] ] + 1 , n e x t [ n e x t [ n e x t [ i − 1 ] ] ] + 1... next[i-1]+1, next[next[i-1]]+1,next[next[next[i-1]]]+1... next[i1]+1,next[next[i1]]+1,next[next[next[i1]]]+1...作为 j j j的选项即可。

其中的候选含义就是满足公共前后缀条件的,当我们发生匹配失败的行为时,不需要从头开始枚举匹配长度,而是从next[j]开始。

求next数组程序如下:

for (int i = 2, j = 0; i <= n; i ++) {
    while (j and p[i] != p[j + 1]) j = ne[j]; // 匹配失败时,缩短匹配长度
    if (p[i] == p[j + 1]) j ++;  // 匹配成功时,匹配长度加1
    ne[i] = j;
}

求出next数组之后就可以使用next数组进行匹配了,过程很简单,类似于求next数组,只不过使用s串和p串进行比较,匹配失败时缩短长度,匹配成功时,长度加1,如果匹配成功的长度到达n说明完全匹配成功

// 匹配过程
    for (int i = 1, j = 0; i <= m; i ++) {
        while (j and s[i] != p[j + 1]) j = ne[j];
        if (s[i] == p[j + 1]) j ++;
        if (j == n) {
            // 匹配成功
            printf ("%d ", i - n + 1); // 输出成功的起始位置
            j = ne[j];
        }
    }

例题

KMP字符串

【题目描述】

给定一个模式串 S,以及一个模板串 P,所有字符串中只包含大小写英文字母以及阿拉伯数字。

模板串 P 在模式串 S 中多次作为子串出现。

求出模板串 P 在模式串 S 中所有出现的位置的起始下标。

【输入格式】

第一行输入整数 N,表示字符串 P 的长度。

第二行输入字符串 P。

第三行输入整数 M,表示字符串 S 的长度。

第四行输入字符串 S。

【输出格式】

共一行,输出所有出现位置的起始下标(下标从 0 开始计数),整数之间用空格隔开。

【数据范围】

1≤N≤10^5
1≤M≤10^6

【输入样例】

3
aba
5
ababa

【输出样例】

0 2

参考程序

#include <iostream>
using namespace std;

const int N = 1e5 + 10, M =1e6 + 10;

int n, m;
char p[N], s[M];
int ne[N];

int main () {
    cin >> n >> p + 1 >> m >> s + 1;
    // 求next 数组
    for (int i = 2, j = 0; i <= n; i ++) {
        while (j and p[i] != p[j + 1]) j = ne[j];
        if (p[i] == p[j + 1]) j ++;
        ne[i] = j;
    }
    
    // 匹配过程
    for (int i = 1, j = 0; i <= m; i ++) {
        while (j and s[i] != p[j + 1]) j = ne[j];
        if (s[i] == p[j + 1]) j ++;
        if (j == n) {
            // 匹配成功
            printf ("%d ", i - n); // 输出成功的起始位置
            j = ne[j];
        }
    }
    
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值