浅谈KMP&扩展KMP

引入

考虑这样一个问题:

给出两个字符串\(S_1,S_2\),求\(S_2\)\(S_1\)出现的所有位置

举例:对于\(S_1=\)ABACABAD\(S_2=\)ABA,显然出现在位置\(1,5\)

ABACABAD
ABA
ABACABAD
    ABA

怎么求解?

很容易想到暴力解法:我们枚举\(S_1\)的每一位作为\(S_2\)的第一位,按位判断是否匹配,若不匹配则移动\(S_2\)至下一位再次判断。

分析一下复杂度?

\(S_1,S_2\)长度为\(n,m\),枚举\(S_1\)的每一位,按位查看,总复杂度很容易卡到\(O(nm)\),在\(n,m\)很大的时候不够优。

所以出现了KMP算法。

Border

在介绍KMP算法之前,我们需要先了解一个字符串的Border。其定义如下:

定义一个字符串 \(s\) 的 border 为 \(s\) 的一个非 \(s\) 本身的子串 \(t\),满足 \(t\) 既是 \(s\) 的前缀,又是 \(s\) 的后缀。

举例:对于字符串ABABA,它有\(2\)border,分别是A,ABA

KMP

KMP算法是一种改进的字符串匹配算法,由\(D.E.Knuth\)\(J.H.Morris\)\(V.R.Pratt\)提出的,所以人们称它为KMP算法

考虑这样一组数据:

ABCABDABCABC
ABCABC

我们按位匹配:

ABCABDABCABC
ABCABC
 ABCABC
  ABCABC
   ABCABC
    ......

不难发现前面几次移动使得第一位都无法匹配(\(A\not =B,A\not =C\)),换句话说就是毫无意义的操作。我们同样看到,移动\(3\)位之后,字符串变得很“匹配”:

ABCABDABCABC
ABCABC

\(\downarrow\)

ABCABDABCABC
   ABCABC

此时前\(2\)位都是相同的。

继续扩展?

每次移动的时候我们贪心地让\(S_2\)的前几位和\(S_1\)当前查看的字串的后几位尽量多的匹配。

换个说法?

每次移动的时候我们让\(S_2\)的前缀和\(S_1\)当前查看的字串的相同长度的后缀相同且长度最大。

前缀等于后缀?这不就是border吗?

我们对于一个字符串\(S\)定义一个next数组,其中\(next_i\)表示\(S[0,i-1]\)中最长的border的长度。

于是我们便得到了KMP算法的核心思想:依据next数组快速的“跳动”进行匹配从而大幅节省时间。

让我们手模一遍过程。

考虑数据\(S_1=\)ABAACABABCAC\(S_2=\)ABABC

先求出\(S_2\)next数组:

\(next=\{0,0,0,1,2\}\)

从头开始匹配:

ABAABABABCAC
ABABC

发现匹配到第\(4\)位的时候出现了问题,查看next数组发现\(next_4=1\),这就意味着前\(3\)位中第一位和最后一位相同,我们可以据此进行“跳跃”,直接把第一位移动到第三位再次匹配:

ABAABABABCAC
  ABABC

此时发现匹配到第\(2\)位的时候出现了问题,继续查看next数组发现\(next_2=0\),前面不支持快速跳跃,所以只后移一位:

ABAABABABCAC
   ABABC

此时发现匹配到第\(5\)位的时候出现了问题,继续查看next数组发现\(next_5=2\),所以我们后移:

ABAABABABCAC
     ABABC

发现匹配成功,输出

一直这样做即可求出答案。

注意到在实现时跳跃完成后无需再次查看之前确定匹配的\(S_2\)的前缀,只需向后查看,同时\(S_1\)也是按位移动查看不会回头,故复杂度线性。

神秘的网站

实现

KMP的思想很简单,但是实现有一定难度

  • 考虑如何求解next数组。

暴力枚举求解next数组的时间复杂度同样是无法接受的,所以我们dp求解。

我们对于字符串\(S\),如果已经求出了\(next_i\),可以求出\(next_{i+1}\)

  • \(S_{i+1}=S_{next_i+1}\)

我们就可以直接把当前位加到border里,即:
\(next_{i+1}=next_i+1\)

当前情况如图所示:
image

  • \(S_{i+1}\not =S_{next_i+1}\)

此时如果还是直接加入的话会出现问题,但是这也同时意味着\(next_{i+1}\)一定小于\(next_i+1\)

我们令\(next_i=k\),则此时问题等价于对于\(S[0,k]\)\(S[i-k+1,i+1]\)求解

同时,因为\(next_i=k\),所以\(S[0,k]\)的前\(next_k\)位和\(S[i-k,i]\)的后\(next_k\)位时一定相同的,我们要使加入新的\(S_{i+1}\)后的border尽可能大就可以考虑\(S_{next_k+1}\)\(S_{i+1}\)是否相同。如果相同即可以取这个较劣但是最大合法的解作为\(next_{i+1}\)

如果不同?

那就递归继续找\(next\)

由于此处较为复杂,若文字看不懂可以看图理解:
image
image

代码实现:

int k = 0;
for (i = 1; i < lenp; ++i) {
    while (k && pat[i] != pat[k]) k = nxt[k];
    if (pat[i] == pat[k])
        nxt[i + 1] = ++k;
    else
        nxt[i + 1] = 0;
}
  • 考虑如何求解

我们在之前叙述思想时所用的“跳跃”“移动”等操作在代码中可以体现为指针的移动

我们使用两个指针,一个指针\(i\)指向\(S_1\),另一个指针\(j\)指向\(S_2\),每次比对\(i,j\)判断是否匹配

我们可以让\(i\)一直线性推进,当不匹配时将\(j\)前移使得整个\(S_2\)后移,从而达到我们的目的

如果当前\(j\)指向\(S_{k}\),那我们让\(j\)变为\(next_k\)即可实现转移。手模过程即可理解。

代码实现:

k = 0;
for (i = 0; i < lent; ++i) {
    while (k && txt[i] != pat[k]) k = nxt[k];
    if (txt[i] == pat[k]) ++k;
    if (k == lenp) printf("%d\n", i - lenp + 2);
}

由此便完成了KMP算法

板子:洛谷P3375 【模板】KMP字符串匹配

#include <bits/stdc++.h>
using namespace std;

char txt[1000005], pat[1000005];
int nxt[1000005];
signed main() {
    scanf("%s%s", &txt, &pat);
    register int i;
    int lent = strlen(txt), lenp = strlen(pat);
    int k = 0;
    for (i = 1; i < lenp; ++i) {
        while (k && pat[i] != pat[k]) k = nxt[k];
        if (pat[i] == pat[k])
            nxt[i + 1] = ++k;
        else
            nxt[i + 1] = 0;
    }
    k = 0;
    for (i = 0; i < lent; ++i) {
        while (k && txt[i] != pat[k]) k = nxt[k];
        if (txt[i] == pat[k]) ++k;
        if (k == lenp) printf("%d\n", i - lenp + 2);
    }

    for (i = 1; i <= lenp; ++i) printf("%d ", nxt[i]);
    return 0;
}

Z函数(扩展KMP)

还是考虑一个问题:

给出字符串\(S\),求\(S\)的每一个后缀与\(S\)的最长公共前缀(LCP)的长度

暴力求解的复杂度是\(O(n^2)\)的,难以接受

我们对于一个字符串\(S\)定义其\(z\)函数,\(z(i)\)表示以\(i\)为开头的后缀与\(S\)LCP的长度。考虑通过dp求得\(z\)函数的值。

给出如下定义:

  • 匹配段(Z-Box):对于\(x\),定义其匹配段为\(S[x,x+z(x)-1]\)

我们看到当前位置\(S_i\),对于所有\(0\le x\le i\)\(l\)的匹配段\(S[x,x+z(x)-1]\)找到右端点最大的一个,不妨设为\(S[l,r]\)

初始时\(l=r=0\)

  • \(i\le r\)

    • \(z(i-l) < r-i+1\)

      由定义可知\(S[i,r]=S[i-l,r-l],\)此时我们直接令\(z(i)=z(i-l)\),理由可以看图理解。
      image

    • \(z(i-l)\ge r-i+1\)

      直接令\(z(i)=r-i+1\),然后向后枚举是否可以扩展。
      image

  • \(i>r\)

暴力向后扩展

全部做完之后,查看\(i+z(i)-1\)是否大于\(r\),若是,则用\(i,i+z(i)-1\)更新\(l,r\)

代码实现:

register int i;
int l = 0, r = 0;
for (i = 1; i < n; ++i) {
    if (i <= r && z[i - l] < r - i + 1) {
        z[i] = z[i - l];
    } else {
        z[i] = max(0, r - i + 1);
        while (i + z[i] < n && s[z[i]] == s[i + z[i]]) ++z[i];
    }
    if (i + z[i] - 1 > r) l = i, r = i + z[i] - 1;
}

注意到\(while\)循环每次执行都会使\(r\)右移至少一位,所以最多执行\(n\)次,同时\(for\)循环均线性,故总复杂度线性。

同时我们可以类似地对于两个字符串\(S,T\),以\(S\)为基础求出\(T\)\(S\)所有后缀的LCP的长度,这就是扩展KMP

板子:洛谷P5410 【模板】扩展 KMP(Z 函数)

注意到这里\(z(0)\)的定义和我们不太一样,特殊处理即可

#include <bits/stdc++.h>
using namespace std;
#define int long long

const int MAXN = 2e7 + 5;
int n, m, z[MAXN], p[MAXN];
char s1[MAXN], s2[MAXN];

inline void Z(char *s, int len) {
    register int i, j = 0;
    int l = 0, r = 0;
    z[0] = len;
    while (j + 1 < len && s[j] == s[j + 1]) ++j;
    z[1] = j;
    for (i = 2; i < len; ++i) {
        if (i <= r && z[i - l] < r - i + 1) {
            z[i] = z[i - l];
        } else {
            z[i] = max(0ll, r - i + 1);
            while (i + z[i] < len && s[z[i]] == s[i + z[i]]) ++z[i];
        }
        if (i + z[i] - 1 > r) l = i, r = i + z[i] - 1;
    }
}

inline void exkmp(char *s1, int len1, char *s2, int len2) {
    register int i, j = 0;
    while (j < len1 && j < len2 && s1[j] == s2[j]) ++j;
    p[0] = j;
    Z(s2, len2);

    int l = 0, r = 0;
    for (i = 1; i < len1; ++i) {
        if (i <= r && z[i - l] < r - i + 1) {
            p[i] = z[i - l];
        } else {
            p[i] = max(0ll, r - i + 1);
            while (i + p[i] < len1 && p[i] < len2 && s2[p[i]] == s1[i + p[i]])
                ++p[i];
        }
        if (i + p[i] - 1 > r) l = i, r = i + p[i] - 1;
    }
}

inline int solve(int *a, int len) {
    int ans = 0ll;
    register int i;

    for (i = 0; i < len; ++i) ans ^= 1ll * (i + 1) * (a[i] + 1);
    return ans;
}

signed main() {
    scanf("%s%s", s1, s2);
    n = strlen(s1), m = strlen(s2);
    exkmp(s1, n, s2, m);

    printf("%lld\n%lld\n", solve(z, m), solve(p, n));
    return 0;
}

posted @ 2022-06-03 15:07  Luisvacson  阅读(151)  评论(0)    收藏  举报