KMP字符串

2021/6/9 15:23:00admin2673 阅读0 点赞0 评论

给定一个模式串 SS,以及一个模板串 PP,所有字符串中只包含大小写英文字母以及阿拉伯数字。
模板串 PP 在模式串 SS 中多次作为字串出现。
求出模板串 PP 在模式串 SS 中所有出现的位置的起始下标。

输入格式

第一行输出整数 NN,表示字符串 PP 的长度。
第二行输入字符串 PP
第三行输入整数 MM,表示字符串 SS 的长度。
第四行输入字符串 MM

输入格式

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

数据范围

1N104\rm{1} \le N \le 10^4
1M105\rm{1} \le M \le 10^5

输入样例

TEXT
3
aba
5
ababa

输出列表

TEXT
0 2

题解

(KMP) O(m+n)O(m+n)
KMP 算法是 D.E.Knuth、J,H,Morris 和 V.R.Pratt 三位神人共同提出的,称之为 Knuth-Morria-Pratt 算法,简称 KMP 算法。该算法相对于 Brute-Force(暴力)算法有比较大的改进,主要是消除了主串指针的回溯,从而使算法效率有了某种程度的提高。

KMP 算法的关键就是求 next 数组:
next 数组长度与字符串长度一致,每个位置存储对应字符的最长匹配长度。
next 数组即 next[i] = length; 长度为 ii 的数组的前缀和后缀相等的最大长度。 例如 abcdeabcabcdeabc 就是 next[8] = 3; 相等的前缀和后缀最长是 abcabc 长度为 3\rm{3}
为了更透彻理解 next 数组,我们来举一个例子
模板串 PP 为:abababababababab(令其下标从 1\rm{1} 开始)
next[1] 即模板串中取长度为 1\rm{1} 的串,如果匹配失败,则退回到起点,故 next[1]=0
next[2] 即模板串中取长度为 2\rm{2} 的串,显然其前缀 aa 和后缀 bb 不相等,故 next[2]=0
next[3] 即模板串中取长度为 3\rm{3} 的串,其前缀 aa 和后缀 aa 相等,aa 的长度为 1\rm{1},故 next[3]=1

\vdots

next[6] 即模板串中取长度为 6\rm{6} 的串(abababababab),其前缀 abababab 和后缀 abababab 相等,该字串长度为 4\rm{4} ,故 next[6]=4,以此类推,next[7]=5

到此,我们应该明白 next 数组的意义了,现在我们加上模式串 SS 来看,假设 SS 为:abababcababababcab(同样令其下标从 1\rm{1} 开始),以下矩阵代表当前两个字符串的对应关系:

[abababcabababababx] \left[ \begin{array}{l} {\rm{a b a b a b c a b}}\\ {\rm{a b a b a b a b x}} \end{array} \right]

xx 代表空格),可见在 s[7] 时 cc 与 模板串 p[j + 1] 中 aa 不匹配,此时,我们如果令 j = next[j],当前 j=6j=6j=next[j]=4 后会发生什么,请看矩阵:

[abababcabxxxxxababababxx] \left[ \begin{array}{l} {\rm{a b a b a b c a b x x x}}\\ {\rm{x x a b a b a b a b x x}} \end{array} \right]

为什么要这样操作?
因为我们 next[i] 长度为 ii 的数组的前缀和后缀相等的最大长度,刚才我们已经判断得到 p[1] $ \sim$ p[6] 一定是和 s[1] \sim s[6] 完全匹配的,又因为 next[i]=4,故 p[1] $ \sim$ p[6]的前 4\rm{4} 个字符与后 4\rm{4} 个字符是完全相同的,也就得到 p[1] $ \sim$ p[4] 与 p[3] $ \sim$ p[6]完全相同,则 p[1] $ \sim$ p[4] 也与 s[3] $ \sim$ s[6] 完全相同(因为 p[1] $ \sim$ p[6] 和 s[1] \sim s[6] 完全匹配),所以我们就没必要再从 p[1] 开始枚举,直接从 p[4 + 1]开始即可。

这样操作我们就省去了主串指针的回溯所花费的不必要的时间,接下来的步骤以此类推,详见 C++ 代码部分的 kmp 匹配过程。

C++ 代码

CPP
#include <iostream>

using namespace std;

const int N = 10010, M = 100010;

int n, m;
char p[N], s[M];
int ne[N]; //next数组

int main()
{
    cin >> n >> (p + 1) >> m >> (s + 1); //下标从1开始
    //求next数组过程
    for (int i = 2, j = 0; i <= n; i++)//i从2开始即可,i=1不匹配的话即是退为0,不考虑
    {
        while(j && p[i] != p[j + 1])
            j = ne[j];
        if(p[i] == p[j + 1])
            j++;
        ne[i] = j;
    }
    //kmp匹配过程
    for (int i = 1, j = 0; i <= m; i++)
    {
        while (j && s[i] != p[j + 1]) //j未回退到起点,且当前不匹配
            j = ne[j];                //ne[j]表示p[j+1]前端和s匹配的最小移动坐标
        if (s[i] == p[j + 1])
            j++;
        if (j == n)
        {
            printf("%d ", i - n);//起始下标
            j = ne[j];
        }
    }
    return 0;
}

评论区