[KMP]模板总结

KMP算法

KMP算法应用场景

主要是文本串和模式串之间的匹配,来加速匹配的算法。
即在线性时间内求出文本串T中查找字符串P,且求出P在T中出现的次数和位置。

朴素的字符串匹配算法

主要是枚举文本串T的每个起始位置i, 然后逐步和字符串P进行比较,如果中途失配,那么i++, 在重新与P进行匹配;

// https://blog.csdn.net/v_july_v/article/details/7041827
int ViolentMatch(char* s, char* p)
{
	int sLen = strlen(s);
	int pLen = strlen(p);
 
	int i = 0;
	int j = 0;
	while (i < sLen && j < pLen)
	{
		if (s[i] == p[j])
		{
			//①如果当前字符匹配成功(即S[i] == P[j]),则i++,j++    
			i++;
			j++;
		}
		else
		{
			//②如果失配(即S[i]! = P[j]),令i = i - (j - 1),j = 0    
			i = i - j + 1;
			j = 0;
		}
	}
	//匹配成功,返回模式串p在文本串s中的位置,否则返回-1
	if (j == pLen)
		return i - j;
	else
		return -1;
}

Brute Force算法需要的时间复杂度为O(mn),mnTP

KMP算法

KMP算法可在O(m+n)时间内完成上述工作。
我觉得KMP算法讲的比较好的是,邓俊辉老师的数据结构课。
https://www.bilibili.com/video/BV1jt4y117KR?p=401

多的也不说,直接开撸。
这里定义的next数组,next[i]表示是除当前字符外的最长相同前缀后缀长度。其实际对应着如果失配,下次需要移动的距离信息(i - next[i])。

特别地,当不存在这样的i时,令next[i] = -1,作为万能哨兵; 其在这里的意义是为了处理当首位置就发生失配的情况,这个时候应该往右移动一个单位。这里不妨假想在P[0]的左侧"附加"P[-1],且该字符与任何字符都匹配,这点和"next[0] = -1"。

//KMP 匹配主算法
//该算法至多一次完全匹配,如果需要统计出现多次,需要将稍微更改模板;
int match(char* P, char* T)
{
    int* next = buildNext(P);  //构建next数组;
    int n = (int)strlen(T), i = 0; //文本串T的指针;
    int m = (int)strlen(P), j = 0;  //模式串P的指针;
    while (j < m && i < n) {
        //匹配,或者P已经移出最左侧;(顺序不可交换)
        if (0 > j || T[i] == P[j]) { 
            i++; j++;
        } else {
            j = next[j]; //当失配时,模式串右移;
        }
    }
    delete[] next;
    return i - j;
}

多次统计模板:

int match(char* P, char* T)
{
    int* next = buildNext(P);  //构建next数组;
    int n = (int)strlen(T), i = 0; //文本串T的指针;
    int m = (int)strlen(P), j = 0;  //模式串P的指针;
    int ans = 0;    //统计出现次数;
    while ( i < n) {
        //匹配,或者P已经移出最左侧;(顺序不可交换)
        if (0 > j || T[i] == P[j]) { 
            i++; j++;
        } else {
            j = next[j]; //当失配时,模式串右移;
        }

        if (j == m) {
            //这里应该是
            //回退一下,因为这个还需要在比较;
            i--, j--;
            j = next[j];
            ans++;
        }
    }
    delete[] next;
    return i - j;
}

next数组求解

  1. next[0] = -1;
  2. 假设已知next[0, j],怎么求出next[j + 1]呢?
    令next[j] = t, 那么说明在P[0, j)中匹配的真前后缀最大长度为t,那么必有next[j + 1] <= next[j] + 1, 等号在P[j] = P[next[j]= t]时取得,可由定义得到。
    对于P[t] != P[j]时,怎么得到P[j + 1]呢?
    由next表的定义,next[j + 1]的定义可得,next[j + 1]的下一个候选者应该依次是:
    next[next[j]] + 1, next[ next[next[j] ] ] + 1, ...
    因为next[t] < t, 所以t在下降过程中始终递减,同时,即便t降低到0,其必然会终止于通配的next[0] = -1,而不会继续往下走。
int* buildNext(char* P)
{
    int m = (int)strlen(P);
    int j = 0;
    int* N = new int[m]; //next表;
    int t =  N[0] = -1;
    //为啥这里是m-1,因为是先++,如果是m的话,继续匹配到m-1时
    //会继续执行,这时经过++, 会变为m,这时就会发生数组越界;
    //在OJ中经常会开辟多余的buffer,所以表现也能正常工作;
    while (j < m - 1) {
        if (t < 0 || P[j] == P[t]) {  //匹配;
            j++; t++;
            N[j] = t;
        }  else { //失配;
            t = N[t];
        }
    }
    return N;
}

//比如,这个帖子中的解答;
https://cloud.tencent.com/developer/article/1525236
平时还是需要注意边界,哪怕多一个少一个也不行;

算法进阶上的模板

  1. KMP算法next数组求解
    1. 初始化 next[1] = 0, 假设 next[1, i - 1]已经求出,下面求解next[i];
    2. 不断尝试扩展匹配长度j,如果扩展失败(下一个字符不相等时),令j变为next[j],直到j变为0(应该从头开始匹配);
    3. 如果能扩展成功,匹配长度j增加1.next[i]的值就是j;

next[1] = 0; //首个位置匹配;
for (int i = 2, j = 0; i <= n; i++) {
    //失配时;
    while (j > 0 && a[i] != a[j + 1] ) {
        j = next[j];
    }
    //此时要么 j == 0,要么a[i] == a[j + 1];
    //如果a[i] == a[j + 1],那么说明可以匹配,则累加;
    if (a[i] == a[j + 1]) {
        j++;
    }
    //当j == 0,说明已经失配,此时需要从头开始匹配;
    //亦或者之前匹配增加的,记录下来;
    next[i] = j;
}

因为这个模板给的是[1, n]上的序列,很容易就可以得到一般程序意义上的[0, n)的模板:

next[0] = 0;
int i = 1, j = 0;
int m = a.size();
while (i < m) {
    while (j > 0 && a[i] != a[j + 1]) {
        j = next[j];
    }

    if (a[i] == a[j + 1]) {
        j++;
    }
    next[i++] = j;
}
  1. KMP算法f数组求法

因为和找next数组类似,所以求解方法很相似:

for (int i = 1, j = 0; i <= m; i++) {
    while (j > 0 && (j == n || b[i] != a[j + 1])) {
        j = next[j];
    }
    if (b[i] == a[j + 1]) {
        j++;
    }
    f[i] = j;
    //if (f[i] == n) 这里就是A在B中的某一次出现;
}

改进版:

//f[i]是字符串B中以i结尾的字符串,能够和字符串A的前缀能够匹配的最长长度;
//B是文本串,A是模式串;
for (int i = 0, j = 0; i < m; i++) {
    while (j > 0 && (j == n || b[i] != a[j + 1])) {
        j = next[j];
    }
    if (b[i] == a[j + 1]) {
        j++;
    }
    f[i] = j;
    // //if (f[i] == n) 这里就是A在B中的某一次出现;
}
int i  = 0, j = 0;
while (i < m) {
    while (j > 0 && (j == n || b[i] != a[j + 1] ) {
        j = next[j];
    }
    if (b[i] == a[j + 1]) {
        j++;
    }
    f[i++] = j;
}

先说个板子题:
HDU1686:

//https://blog.csdn.net/qq_40924940/article/details/83964199
#include<cstdio>
#include<cstring>
#include<iostream>
#include<cmath>
using namespace std;
const int maxn = 1e6 +50;
int nex[maxn];
char c1[maxn],c2[maxn];
void getnex()
{
    int len = strlen(c1);
    nex[0]=-1;
    int j=0;
    int k=-1;
    while(j<len-1)
    {
        if(k==-1||c1[k]==c1[j])
            nex[++j]=++k;
        else
            k=nex[k];
    }
}
int k;
int kmp()
{
    int i=0,j=0;
    int k=0;
    int l1 = strlen(c1);
    int l2 = strlen(c2);
    getnex();
    while(i<l2)
    {
        if(j == -1 || c2[i]==c1[j])
        {
            i++;
            j++;
        }
        else
            j = nex[j];
        if(j == l1)
        {
            k++;
            i--,j--;
            j=nex[j];
        }
    }
    return k;
}
int main()
{
    int T;
    scanf("%d",&T);
    while(T--)
    {
        scanf("%s%s",c1,c2);
        printf("%d\n",kmp());
    }
    return 0;
}

至此,普通KMP总结完毕,对于一般的OI/ACM基本已经够用。其实可以有优化的KMP及BM算法,以及效果更好Sunday算法。但是先把题刷起来。

posted @   zhanghanLeo  阅读(187)  评论(0编辑  收藏  举报
编辑推荐:
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示