学习NOTE 6——KMP

KMP Loading. . . . . .

是什么

KMP 算法是一种改进了的字符串匹配算法,用来快速找出一个短字符串(模式串)在另一个长字符串(文本串)中出现的所有位置。这个算法是由$ \textbf{D.E.Knuth,J.H.Morris 和 V.R.Pratt} $这三个大神犇发明的。

为什么叫“改进了的”? 因为KMP可以用\(O(n + m)\)的时间复杂度完成匹配,远胜于暴力查找的\(O(n \times m)\)牛逼!!(其中\(n\)\(m\)指文本串长度和模式串长度,下同)

字符串匹配

我们之前学过的暴力是这样的:

for(int i=0;i+t.size()-1<s.size();i++){
	bool flg=1;
	for(int j=0;j<t.size();j++){
		if(s[i+j-1]!=t[j]){
			flg=0;
			break;
		}
	}
	if(flg) cout<<i<<" "<<i+t.size()-1<<"\n";
}

时间复杂度为\(O(n \times m)\)(最坏情况)。

举个栗子:

假设:

  • 文本串 s = "AAAAAB"
  • 模式串 t = "AAAAB"

暴力匹配的做法:

  1. 从s的第1位开始比较:
  • \(A=A ✓,A=A ✓,A=A ✓,A=A ✓,\color{Red}B \ne A ✗\)(第5位失败)
  • 然后回到s的第2位,重新从t的第1位开始比较...
  1. 这样一直试到第2位才成功。

问题:每次匹配失败,文本串指针都要回退,导致很多字符被重复比较。最坏情况下时间复杂度是 \(O(n \times m)\)(n是文本长度,m是模式长度),当n和m都很大时(比如\(10^6\)),会超时。

KMP代码实现

匹配过程中已经比较过的文本字符,不要浪费! 即使某次匹配失败了,我们也知道"已经匹配了多长"和"这些匹配上的字符是什么",可以利用这个信息让模式串跳着走,而不是从头开始。

这时,我们需要记录一个\(p\)数组,用来表示子串 \(t_{0..i}\) 中,最长的相等前缀和后缀的长度(且长度小于 \(i+1\)

也就是说:当匹配到模式串的 \(t_i\) 时失败了,说明前面的 \(t_{0..i-1}\) 都匹配成功了。那我们可以把模式串往后移动一段距离,移动多少呢?移动 \(i - p_{i-1}\) 位,也就是让模式串的 \(p_{i-1}\) 位置对准当前文本位置继续比较。

栗子

  • 文本串 s = "ABABABC"
  • 模式串 t = "ABABC"
第一步:先算模式串的前缀函数

t = "ABABC"

  • p[0]=0(单字符)
  • p[1]=0("AB",前缀"A"≠后缀"B")
  • p[2]=1("ABA",前缀"A"=后缀"A")
  • p[3]=2("ABAB",前缀"AB"=后缀"AB")
  • p[4]=0("ABABC",没有相等的前后缀)

所以 p = [0, 0, 1, 2, 0]

第二步:匹配(重点!)
s: A B A B A B C
t: A B A B C
  1. i=0, j=0:\(A=A\) ✓,i=1, j=1
  2. i=1, j=1:\(B=B\) ✓,i=2, j=2
  3. i=2, j=2:\(A=A\) ✓,i=3, j=3
  4. i=3, j=3:\(B=B\) ✓,i=4, j=4
  5. i=4, j=4:$A \ne C $✗(失配!)

暴力做法:回退到s的第2位(B),t从头(A)开始。

KMP做法

  • 当前j=4(匹配了4个字符,"ABAB"
  • 查表:p[3] = 2(因为失配在t[4],我们看t[0..3]的最长相等前缀和后缀长度)
  • j = p[3] = 2i不变(i=4)
  • 现在比较 s[4]='A't[2]='A'
  1. i=4, j=2:\(A=A\) ✓,i=5, j=3
  2. i=5, j=3:\(B=B\) ✓,i=6, j=4
  3. i=6, j=4:\(C=C\) ✓,匹配成功!位置 = 6-5+1 = 2(从1开始计数)

See? 文本串指针i从来没有回退,只是模式串的指针\(j\)跳到了\(2\),省去了重复比较"AB"的时间。

代码

PID:P3375

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e6+10;
int p[N];//存储前缀函数
string s,t;
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>s>>t;
	int n=s.size(),m=t.size();
	for(int i=1,j=0;i<m;i++){//前缀函数
		while(j>0&&t[i]!=t[j]) j=p[j-1];//失配时回退j
		if(t[i]==t[j]) j++;//匹配则前进
		p[i]=j;
	}
	for(int i=0,j=0;i<n;){//j:字符串t的下标 i:字符串s的下标
		if(s[i]==t[j]) i++,j++;
		else if(j>0) j=p[j-1];//如果字符串不匹配重新计算
		else i++;//继续往后延伸字符串s
		if(j==m){//如果成功匹配字符串
			j=p[j-1];//回退模式串指针,目的是为了寻找下一个
			cout<<i-m+1<<"\n";//输出匹配起始位置
		}
	}
	for(int i=0;i<m;i++) cout<<p[i]<<" ";
	return 0;
}
posted @ 2026-07-28 11:14  _T_M_T  阅读(3)  评论(0)    收藏  举报