KMP 算法

KMP

给一个待匹配的文本串和一个需要在文本中搜索的模式串,在文本串中,模式串出现的次数、位置等



  1. 朴素算法:枚举每个文本串元素,从这一位开始比较,每次失败就从头开始比对,

    很容易可以把这个算法卡成 \(O(nm)\)

  2. KMP 算法思想:每次失败,不会从头开始枚举,而是从某个特定位置开始

    模式串的每一位都有固定的变化值,每次失配后不用从头匹配,从而节省时间。比如

    模式串:abcabc
    文本串:abcabdababcabc
    

    第六位失配了,直接将模式串移动到文本串的第 4 位

    模式串:   abcabc
    文本串:abcabdababcabc
    
  3. 移位法则:在模式串 s 中,对于一个 \(s_i\) 应该记录跳转位置 \(nxt_i=j\) 使得 \(j<i,s_i=s_j\)

    且对于 \(j\ne1\)\(s_1\) ~ \(s_{j-1}\)\(=s_{i-j+1}\) ~ \(s_{i-1}\) 按位相等

    其实就是用 \(nxt\) 数组记录模式串到 \(i\) 为止真前缀和真后缀最大相同位置

实现

匹配部分

比较简单

for(int i=0,j=0;i<=n;i++) {
	while(j && x[i]!=y[j+1])j=nxt[j];
	if(x[i]==y[j+1])++j;
	if(j==m) {
		printf("%d\n",i-m+1); 
		j=nxt[j];
	}
}

求 nxt 数组部分

考虑自己匹配自己

for(int i=2,j=0;i<=m;i++) {
	while(j && y[i]!=y[j+1])j=nxt[j];
	if(y[i]==y[j+1])++j;
	nxt[i]=j;
}
  1. 每次最多向后匹配一位 nxt

  2. j 用于比对前后缀,对于一对前后缀而言,第 i-1 和 j-1 位前都有异同,

    这决定 i 与 j 的匹配结果从哪开始

Code

Luogu P3375

#include<bits/stdc++.h>
using namespace std;
const int N=1000005;
int n,m,nxt[N];
char x[N],y[N];
int main()  {
	scanf("%s%s",x+1,y+1);
	n=strlen(x+1),m=strlen(y+1);
	for(int i=2,j=0;i<=m;i++) {
		while(j && y[i]!=y[j+1])j=nxt[j];
		if(y[i]==y[j+1])++j;
		nxt[i]=j;
	}
	for(int i=0,j=0;i<=n;i++) {
		while(j && x[i]!=y[j+1])j=nxt[j];
		if(x[i]==y[j+1])++j;
		if(j==m) {
			printf("%d\n",i-m+1); 
			j=nxt[j];
		}
	}
	for(int i=1;i<=m;i++)printf("%d ",nxt[i]);
}

时间复杂度

对于每一个 i ,j 至多增加 m 次,所以至多减少 m 次,为 \(O(n+m)\)

ex KMP

扩展 \(\text{KMP}\) 算法,求模式串的每一个后缀与文本串的最大公共前缀,在 \(O(n+m)\) 的时间内

分两部分求解

此处假设 \(S\) 为文本串,\(T\) 为模式串

z 函数

\(z_i\) 表示 \(T\)\(i\) 开头的后缀与 \(T\) 自己的最大公共前缀。

假设当前 \(z_{1,2,\cdots,x-1}\) 已经求出,考虑求 \(z_x\)

做法:对于 \(i<x\) ,记录 \(i+z_i-1\) 的最大值 \(r\) 以及 \(l\) 为其对应的 \(i\)

\(z\) 的定义可得 \([l,r]\)\([1,z_l]\) 对应相等

  • \(x\le r\)

    \([l,r]\) 中找到 \(x\) 的对应位置,可得蓝色段相等

    \(z_{x-l+1}\) 的定义,可得红色部分相等

    1. \(i+z_{x-l+1}\le r\)\(z_x=z_{x-l+1}\)
    2. \(i+z_{x-l+1}>r\) ,则我们只知道 \(z_x\) 至少为 \(r-x+1\) ,剩下的暴力向后扩展
  • \(x>r\) 啥都不知道,直接扩展

实现:

int z[N];
char b[N];
z[1] = m;
for (int i = 2, l = 0, r = 0; i <= m; i++) {
	if (i <= r) z[i] = min(z[i - l + 1], r - i + 1);
    while (i + z[i] <= m && b[i + z[i]] == b[z[i] + 1]) ++z[i];
    if (i + z[i] - 1 > r) r = i + z[i] - 1, l = i;
}

无论是思路、实现还是复杂度分析,都很像 manacher 算法。

复杂度显然 \(O(n)\)

ex 数组

\(ex_i\) 就是 \(T\)\(i\) 开头的后缀与 \(S\) 的最大公共前缀

原理类似 \(z\) 函数,记录 \(i+ex_i-1\) 最大值 \(r\) 以及对应 \(l\)

放个图吧

依然可以分类讨论 + 暴力扩展完成 \(O(n)\) 解决

char a[N];
int ex[N];
for (int i = 1, l = 0, r = 0; i <= n; i++) {
    if (i <= r) ex[i] = min(z[i - l + 1], r - i + 1);
    while (i + ex[i] <= n && a[i + ex[i]] == b[ex[i] + 1]) ++ex[i];
    if (i + ex[i] - 1 > r) r = i + ex[i] - 1, l = i;
}

例题

Luogu 5410

#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long uLL;
typedef long double LD;
typedef long long LL;
typedef double db;
const int N = 2e7 + 5;
int n, m, z[N], ex[N];
LL ans;
char C, a[N], b[N];
int main() {
    C = getchar();
    for (; C < 'a' || C > 'z'; C = getchar());
    for (; C > '`' && C < '{'; C = getchar()) a[++n] = C;
    C = getchar();
    for (; C < 'a' || C > 'z'; C = getchar());
    for (; C > '`' && C < '{'; C = getchar()) b[++m] = C;
    z[1] = m;
    for (int i = 2, l = 0, r = 0; i <= m; i++) {
        if (i <= r) z[i] = min(z[i - l + 1], r - i + 1);
        while (i + z[i] <= m && b[i + z[i]] == b[z[i] + 1]) ++z[i];
        if (i + z[i] - 1 > r) r = i + z[i] - 1, l = i;
    }
    ans = 0;
    for (int i = 1; i <= m; i++) ans ^= 1ll * i * (z[i] + 1);
    printf("%lld\n", ans);
    for (int i = 1, l = 0, r = 0; i <= n; i++) {
        if (i <= r) ex[i] = min(z[i - l + 1], r - i + 1);
        while (i + ex[i] <= n && a[i + ex[i]] == b[ex[i] + 1]) ++ex[i];
        if (i + ex[i] - 1 > r) r = i + ex[i] - 1, l = i;
    }
    ans = 0;
    for (int i = 1; i <= n; i++) ans ^= 1ll * i * (ex[i] + 1);
    printf("%lld\n", ans);
}
posted @ 2021-02-09 23:31  小蒟蒻laf  阅读(57)  评论(0编辑  收藏  举报