不得不承认,@皎月半洒花大佬讲的比我规范,但看不懂的话可以看看我的。

用途

在字符串初级算法里,而其中最经典的模型问题就是判断一个串是否是另一个串的子串。我们常用 KMP 算法解决这类问题。

题目描述

给定两个字符串 s 1 s_1 s1 s 2 s_2 s2,求出 s 2 s_2 s2 s 1 s_1 s1 中所有出现的位置和 s 2 s_2 s2 的每个前缀 s ′ s' s 的最长 border 的长度。。

在本题解中,我用 n n n 代替 ∣ s 1 ∣ |s_1| s1(即 s 1 s_1 s1 的长度),用 m m m 代替 ∣ s 2 ∣ |s_2| s2

暴力匹配

枚举 s 1 s_1 s1 的每一个起始位置 i ( 0 ≤ i < n − m ) i(0\le i<n-m) i(0i<nm),看从 i i i 位置开始往后长度为 m m m 的子串是否是可以和 s 2 s_2 s2 匹配

判断两个串匹配成功的条件是:两个串长度一致,两个串每一个对应位置字符都完全一样。

本做法时间复杂度为 O ( n m ) O(nm) O(nm),只能得 70 分,比总司令高

更优做法:KMP

定义

定义一个字符串性质——前缀后缀最大值,即题目中要求的 border(让你求那多半要用)。

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

我将字符串 s s s 的前缀 s ′ s' s 的最长 border 的长度称为 n e x t ∣ s ′ ∣ next_{|s'|} nexts别问我为啥

换句话说, n e x t i next_i nexti 就是遍历到字符串 s 2 s_2 s2 的第 i i i 位时,与已经匹配成功的部分的后缀相同的最大的长度。我代码中的 kmp[i] 就是 n e x t i next_i nexti

匹配过程

举例说明,其中 s 1 s_1 s1ABACABACABD s 2 s_2 s2ABACABD

先将 s 1 s_1 s1 s 2 s_2 s2 的第一位对齐并看对应位是否相同,我们发现一路畅通,但最后一个匹配失败了。若这时对齐 s 1 s_1 s1 的第二位和 s 2 s_2 s2 的第一位将 s 2 s_2 s2 重新匹配一遍就成了暴力。

      i
ABACABACABD
ABACABD
      j

仔细回想 n e x t next next 定义发现:失配后,我们可以直接跳到 n e x t j next_j nextj,即与已匹配部分相同的 s 2 s_2 s2 最大前缀的最后一位的下一位,手动得出此时 n e x t j = 2 next_j=2 nextj=2。然后就会变成这样:

      i
ABACABACABD
    ABACABD
      j

这样做,复杂度为 O ( n + m ) O(n+m) O(n+m)

n e x t next next

s 2 s_2 s2 自己和自己匹配,过程和上面相仿。匹配到 j j j 失败时,记录 n e x t i next_i nexti j j j。匹配时一个字符串不动,另一个向后移, i i i 为匹配到不动的 s 2 s_2 s2 的位置, j j j 为匹配到要动的 s 2 s_2 s2 的位置。

代码实现

#include<bits/stdc++.h>
using namespace std;
const int N=1e6+2;
int kmp[N],la,lb,j;
//kmp即Next数组,表示相同前缀后缀最大值(不是对称的最大值)
string a,b,A,B;

int main(){
   
	cin>>A>>B;
	a='\0'+A,b='\0'+B;
	la=a.size()-1,lb=b.size()-1;
	for(int i=2;i<=lb;i++){
   //求kmp
		while(j&&b[i]!=b[j+1]) j=kmp[j];
		if(b[j+1]==b[i]) j++;
		kmp[i]=j;
	}
	j=0;
	for(int i=1;i<=la;i++){
   //匹配
		while(j>0&&b[j+1]!=a[i]) j=kmp[j];
		if(b[j+1]==a[i]) j++;
		if(j==lb){
   printf("%d\n",i-lb+1);j=kmp[j];}
	}
	for(int i=1;i<=lb;i++) printf("%d ",kmp[i]);
	return 0;
}