In solitute,where we are least alone.|

Luisvacson

园龄:3年6个月粉丝:5关注:0

浅谈KMP&扩展KMP

引入

考虑这样一个问题:

给出两个字符串S1,S2,求S2S1出现的所有位置

举例:对于S1=ABACABADS2=ABA,显然出现在位置1,5

ABACABAD
ABA
ABACABAD
    ABA

怎么求解?

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

分析一下复杂度?

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

所以出现了KMP算法。

Border

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

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

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

KMP

KMP算法是一种改进的字符串匹配算法,由D.E.KnuthJ.H.MorrisV.R.Pratt提出的,所以人们称它为KMP算法

考虑这样一组数据:

ABCABDABCABC
ABCABC

我们按位匹配:

ABCABDABCABC
ABCABC
 ABCABC
  ABCABC
   ABCABC
    ......

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

ABCABDABCABC
ABCABC

ABCABDABCABC
   ABCABC

此时前2位都是相同的。

继续扩展?

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

换个说法?

每次移动的时候我们让S2的前缀和S1当前查看的字串的相同长度的后缀相同且长度最大。

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

我们对于一个字符串S定义一个next数组,其中nexti表示S[0,i1]中最长的border的长度。

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

让我们手模一遍过程。

考虑数据S1=ABAACABABCACS2=ABABC

先求出S2next数组:

next={0,0,0,1,2}

从头开始匹配:

ABAABABABCAC
ABABC

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

ABAABABABCAC
  ABABC

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

ABAABABABCAC
   ABABC

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

ABAABABABCAC
     ABABC

发现匹配成功,输出

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

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

神秘的网站

实现

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

  • 考虑如何求解next数组。

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

我们对于字符串S,如果已经求出了nexti,可以求出nexti+1

  • Si+1=Snexti+1

我们就可以直接把当前位加到border里,即:
nexti+1=nexti+1

当前情况如图所示:
image

  • Si+1Snexti+1

此时如果还是直接加入的话会出现问题,但是这也同时意味着nexti+1一定小于nexti+1

我们令nexti=k,则此时问题等价于对于S[0,k]S[ik+1,i+1]求解

同时,因为nexti=k,所以S[0,k]的前nextk位和S[ik,i]的后nextk位时一定相同的,我们要使加入新的Si+1后的border尽可能大就可以考虑Snextk+1Si+1是否相同。如果相同即可以取这个较劣但是最大合法的解作为nexti+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指向S1,另一个指针j指向S2,每次比对i,j判断是否匹配

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

如果当前j指向Sk,那我们让j变为nextk即可实现转移。手模过程即可理解。

代码实现:

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(n2)的,难以接受

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

给出如下定义:

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

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

初始时l=r=0

  • ir

    • z(il)<ri+1

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

    • z(il)ri+1

      直接令z(i)=ri+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为基础求出TS所有后缀的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 @   Luisvacson  阅读(116)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起