P3375 KMP 题解

· · 题解

使用背景

给定两个字符串 s_1s_2,需要找到 s_2s_1 中出现的所有位置。

暴力求解

我们很容易想到的就是匹配字符串时,我们从目标字符串 长度为 ns_1 的第一个下标选取和长度为 ms_2 长度一样的子字符串进行比较,如果一样,就返回开始处的下标值,不一样,选取 s_1 下一个下标,同样从 s_2 选取长度为 n 的字符串进行比较,直到 s_1 的末尾。

显然,我们发现了许多不合理的操作:比对失败时会从头开始匹配,浪费了许多时间,时间复杂度为 O(nm)

KMP 算法

根据上述原因,我们可以进一步优化。

在比对失败之后,如果可以向后移动多位,就可以减少不少时间

代码实现

  1. 求 next 数组(建议写成 nxt

      int len1 = s1.size(), len2 = s2.size();
      int j = 0;
      for (int i = 1; i < len2; i++)
      {
          while (j > 0 && s2[i] != s2[j]) j = nxt[j - 1];
          if (s2[i] == s2[j]) j++;
          nxt[i] = j;
      }

    外围的循环就是遍历整个 s_2,从 1 开始寻找的原因就是刚刚所说的,单个字符无法构成最长前缀,直接从第二个字符开始查找
    内层循环,如果 j 此时大于 0,并且 s2_is2_j 不匹配,那么就进行回溯(j = nxt_{j-1}
    进入到下面的判断,此时如果 s2_i 等于 s2_jj 加一。
    此时再将 nxt_j 赋值为 j也就是相同的最长前缀和最长后缀的长

  2. 进行 KMP。

      for (int i = 0; i < len1; i++)
      {
         while (j > 0 && s1[i] != s2[j]) j = nxt[j - 1];
         if (s2[j] == s1[i]) j++;
         if (j == len2) cout << (i + 1) - len2 + 1 << endl, j = nxt[j - 1]; 
      }

    其实跟求 nxt 数组的过程差不多。只是我们不需要再更改 nxt 数组,而是当 j 等于 s_2 的长度时输出匹配成功的第一个字符的位置即可,再更新 j

这就是 KMP 算法的整个流程。

参考代码(模板)

#include <bits/stdc++.h>
using namespace std;
int nxt[1000005];

int main()
{
    string s1, s2;
    cin >> s1 >> s2;
    int len1 = s1.size(), len2 = s2.size();
    int j = 0;
    for (int i = 1; i < len2; i++)
    {
        while (j > 0 && s2[i] != s2[j]) j = nxt[j - 1];
        if (s2[i] == s2[j]) j++;
        nxt[i] = j;
    }
    j = 0;
    bool flag = false;
    for (int i = 0; i < len1; i++)
    {
        while (j > 0 && s1[i] != s2[j]) j = nxt[j - 1];
        if (s2[j] == s1[i]) j++;
        if (j == len2) cout << (i + 1) - len2 + 1 << endl, j = nxt[j - 1], flag = true; 
    }
    for (int i = 0; i < len2; i++) cout << nxt[i] << ' ';
    return 0;
} 

时间复杂度

在预处理阶段,我们生成前缀函数这一步为 O(n)。后面搜索阶段,就算每次都不匹配,最坏情况下也只有 O(m)。因此,KMP 算法的时间复杂度为 O(n + m)