【ACWing】831. KMP字符串

该博客详细介绍了KMP算法在字符串匹配中的应用,包括如何构建next数组以及如何使用KMP算法找到模式串在目标串中的所有出现位置。文章讨论了未优化和优化的next数组在匹配过程中的区别,并提供了相应的C++代码实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

题目地址:

https://www.acwing.com/problem/content/description/833/

给定一个串 s s s和一个模式串 p p p,要求输出所有 p p p作为子串在 s s s中出现的首字母的位置。下标从 0 0 0开始计数。

输入格式:
第一行输入整数 N N N,表示字符串 P P P的长度。第二行输入字符串 P P P。第三行输入整数 M M M,表示字符串 S S S的长度。第四行输入字符串 S S S

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

数据范围:
1 ≤ M ≤ 1 0 5 1\le M\le 10^5 1M105
1 ≤ N ≤ 1 0 6 1\le N\le 10^6 1N106

可以用KMP算法。开两个指针,指针 i i i指向 s s s,另一个指针 j j j指向 t t t。我们想让 i i i不回退,而只让 j j j回退。考虑当发生不匹配的时候, j j j应该要回退到什么位置继续比较。例如 s [ a ] ≠ p [ b ] s[a]\ne p[b] s[a]=p[b],那么我们起码知道了 s [ a − b : a − 1 ] = p [ 0 : b − 1 ] s[a-b:a-1]=p[0:b-1] s[ab:a1]=p[0:b1],而 j j j回退的位置应当使得 p [ 0 : j − 1 ] = p [ b − j : b − 1 ] p[0:j-1]=p[b-j:b-1] p[0:j1]=p[bj:b1]第一次成立,即 j j j要取最大的满足这个条件的数(这里的第一次成立的意思是,从 j − 1 j-1 j1 0 0 0数的第一次,这样 j j j回退的最少。之所以这样做,是在模仿回退 1 , 2 , . . . 1,2,... 1,2,...步,第一次发现 s [ a − j : a − 1 ] = p [ 0 : j − 1 ] s[a-j:a-1]=p[0:j-1] s[aj:a1]=p[0:j1]成立的情形)。对于每个 j j j,它失配后转移的位置即为 n [ j ] n[j] n[j](即为KMP算法的next数组),那么显然,这里 n [ j ] n[j] n[j]的含义就是,使得 p [ 0 : k − 1 ] = p [ j − k : j − 1 ] p[0:k-1]=p[j-k:j-1] p[0:k1]=p[jk:j1]并且 k < j k<j k<j的最大的 k k k。直观理解就是, n [ j ] n[j] n[j]是使得 p [ 0 : j − 1 ] p[0:j-1] p[0:j1]的前后缀相等的最大长度。这样在做双指针匹配的时候,当 s [ a ] ≠ p [ b ] s[a]\ne p[b] s[a]=p[b]的时候,可以查表,令 b = n [ b ] b=n[b] b=n[b],然后继续匹配 s [ a ] s[a] s[a] p [ b ] p[b] p[b]。当然如果 s [ a ] = p [ b ] s[a]= p[b] s[a]=p[b],那就直接两个指针同时后移。当发现 j = l p j=l_p j=lp说明找到了一次匹配,起始下标就是 i − j i-j ij

这里还有两个问题需要解决:
1、怎么计算 n n n数组。首先规定 n [ 0 ] = − 1 n[0]=-1 n[0]=1,这里表示的意思是,如果 p [ 0 ] p[0] p[0]就直接不匹配 s [ i ] s[i] s[i]了,那就将 j j j回退到 − 1 -1 1,下一轮再去匹配 s [ i + 1 ] s[i+1] s[i+1] p [ 0 ] p[0] p[0],这样正好方便代码的书写。接下来我们考虑 n n n数组怎么递推。其实递推 n n n数组本质上也是在做字符串匹配,只不过是 p p p自己的后缀和自己的前缀匹配。我们考虑如果 p [ i ] p[i] p[i]要和 p [ j ] p[j] p[j]进行匹配,并且 p [ i − j : i − 1 ] = p [ 0 : j − 1 ] p[i-j:i-1]=p[0:j-1] p[ij:i1]=p[0:j1],那么如果 p [ i ] = p [ j ] p[i]=p[j] p[i]=p[j],说明前后缀又多了一个字符可以匹配,所以 n [ i + 1 ] = n [ j + 1 ] n[i+1]=n[j+1] n[i+1]=n[j+1];否则 j j j回退到 n [ j ] n[j] n[j],去匹配下一个前缀 p [ 0 : n [ j ] ] p[0:n[j]] p[0:n[j]]。这里的原理和匹配 s s s p p p的过程是一样的。同样如果 j = − 1 j=-1 j=1了,说明此时不存在相等的前后缀,则下一次匹配位置就是 p [ 0 ] p[0] p[0],即 n [ i + 1 ] = 0 n[i+1]=0 n[i+1]=0

2、怎么找到满足条件的所有匹配。这个很容易处理,只需要在找到匹配的时候,先存下答案,然后让 i i i回退一步,让 j j j等于 n [ j − 1 ] n[j-1] n[j1],假装 s [ i − 1 ] s[i-1] s[i1] p [ j − 1 ] p[j-1] p[j1]不匹配就行了。

代码如下:

#include <iostream>
#include <string>
#include <vector>
using namespace std;

string s, p;
int n, m;
vector<int> res;

void buildNext(string p, int ne[]) {
  ne[0] = -1;
  for (int i = 0, j = -1; i < m - 1;)
    if (j < 0 || p[j] == p[i])
      ne[++i] = ++j;
    else
      j = ne[j];
}

void kmp(string s, string p) {
  int ne[m];
  buildNext(p, ne);
  for (int i = 0, j = 0; i < n;) {
    if (j == -1 || s[i] == p[j]) {
      i++, j++;
      if (j == m) {
        cout << i - j << " ";
        i--;
        j = ne[j - 1];
      }
    } else
      j = ne[j];
  }
}

int main() {
  cin >> m;
  cin >> p;
  cin >> n;
  cin >> s;

  kmp(s, p);
}

时间复杂度 O ( N + M ) O(N+M) O(N+M),空间 O ( M ) O(M) O(M)

下面考虑 n n n数组的优化(但是需要注意,优化后的 n n n数组是做不了这道题的,只能做到找到首次匹配的位置)。有一种情况是,如果 p [ j ] p[j] p[j]失配,然而 p [ n [ j ] ] = p [ j ] p[n[j]]=p[j] p[n[j]]=p[j],那么再比较一次必然还是不匹配的,这时候相当于又做了次无用功。这时我们可以在求 n n n数组的时候加一个判断,在执行 i i i j j j自增后,如果发现 p [ i ] ≠ p [ j ] p[i]\ne p[j] p[i]=p[j]才令 n [ i ] = j n[i]=j n[i]=j,表达的意思是如果不等,才可以像上面的做法那样前后缀匹配长度加 1 1 1;如果发现 p [ i ] = p [ j ] p[i]= p[j] p[i]=p[j]了,那像上面做法那样回退是没用的,因为回退了还是不匹配,必须从 j j j再回退一步到 n [ j ] n[j] n[j]才行。

之所以说优化后 n n n数组无法做这道题,原因是优化里考虑了转移之后依然不匹配的情况了。这就会造成,如果是匹配的,我们还像上面那样假装最后一个字符不匹配而调用优化版 n n n数组进行跳转,就会直接把明明匹配的字符给跳掉。这样就找不到所有的匹配情况了。换句话说,未优化的 n n n数组只考虑了当前不匹配的话, p p p要至少右移多少。而优化的 n n n还考虑了万一右移了还不匹配怎么办。这就是优化版无法做这道题的原因(当然也不是说做不了,只不过最坏情况会退化成暴力匹配,然后TLE)。

代码如下(找到第一次匹配位置):

void buildNext(string p, int ne[]) {
  ne[0] = -1;
  for (int i = 0, j = -1; i < m - 1;)
    if (j < 0 || p[j] == p[i]) {
      i++;
      j++;
      ne[i] = p[i] != p[j] ? j : ne[j];
    } else j = ne[j];
}

void kmp(string s, string p) {
  int ne[m];
  buildNext(p, ne);
  for (int i = 0, j = 0; i < n;) {
    if (j == -1 || s[i] == p[j]) i++, j++;
    else j = ne[j];

	// 找到第一次匹配位置即输出答案并退出
    if (j == m) {
      cout << i - j << endl;
      break;
    }
  }
}

提供一个字符串下标从 1 1 1开始的模板:

#include <iostream>
using namespace std;

const int N = 1e6 + 10;
int n, m;
char p[N], s[N];
int ne[N];

void build_ne() {
  for (int i = 2, j = 0; i <= m; i++) {
    while (j && p[i] != p[j + 1]) j = ne[j];
    if (p[i] == p[j + 1]) j++;
    ne[i] = j;
  }
}

int main() {
  cin >> m >> p + 1 >> n >> s + 1;

  build_ne();

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

时空复杂度与上同。

优化版:

void build_ne() {
  for (int i = 2, j = 0; i <= m; i++) {
    while (j && p[i] != p[j + 1]) j = ne[j];
    if (p[i] == p[j + 1]) j++;
    ne[i] = i < m && p[i + 1] != p[j + 1] ? j : ne[j];
  }
}

主函数部分一样。

当然字符串哈希做法也可以,代码如下:

#include <iostream>
using namespace std;
using ll = long long;

const int N = 1e6 + 10;
const ll P = 131;
int n, m;
char s[N], p[N];
ll hashP, hashS, po = 1;

int main() {
  scanf("%d", &m);
  scanf("%s", p + 1);
  scanf("%d", &n);
  scanf("%s", s + 1);

  for (int i = 1; i <= m; i++) {
    hashP = hashP * P + p[i];
    po = po * P;
  }

  for (int i = 1; i <= n; i++) {
    hashS = hashS * P + s[i];
    if (i >= m) {
      hashS -= s[i - m] * po;
      if (hashS == hashP) printf("%d ", i - m);
    }
  }
}

时间复杂度 O ( N + M ) O(N+M) O(N+M),空间 O ( 1 ) O(1) O(1)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值