字符串(3) KMP

说句闲话,KMP 是Knuth、Morris 和 Pratt共同发布的算法,所以叫做 KMP。

基本原理

KMP 算法的主要作用其实就是在字符串中查找子串,并且由于 KMP 用到了前缀函数这个东西,KMP 有时能做一些哈希做不了的东西。

让我们思考一个暴力的查找子串的方法:对于主串的每一个位置进行暴力匹配。

但显然这个东西的时间复杂度在绝大多数题目中都是不可接受的,于是我们引入前缀函数来解决这个问题了。前缀函数被定义为一个长度为 \(n\) 的数组 \(\pi\),其中 \(\pi_i\) 表示:在原字符串 \(s\)\([1,i]\) 中,最长的真前缀和真后缀相等的长度。(更形式化的定义在

让我们思考如何用前缀函数来优化暴力。令 \(S\) 表示主串,用 \(s\) 表示与主串匹配的字符串。我们如果在 \(S_i\)\(s_j\) 的匹配中失配,我们可以对于子串的指针 \(j\) 进行向前跳跃到 \(\pi_{j-1}\),直到 \(S_i = s_j\) 或者 \(j = 0\) 时,再继续匹配 \(S_{i+1}\)

假设 \(\pi_j = k\),则我们可以知道 \(s_{1...k} = s_{j-k+1...j}\),显然匹配过相同的部分没有必要继续匹配,所以我们直接匹配的操作是正确的。关于这一项的图文解释,其实已经存在非常好的博客了,这里引下链接。(当然也是因为懒)

我们考虑完了使用前缀函数,让我们再考虑如何构造前缀函数。我们定义两个指针 \(i\)\(j\) 为当前求的是 \(\pi_i\)\(j = \pi_{i-1}\)

  • 如果 \(s_j = s_i\),则 \(\pi_i = \pi_{i-1} + 1\)

  • 如果 \(s_j \ne s_i\),则 \(j\) 进行向前跳跃到 \(\pi_{j-1}\),直到 \(S_i = s_j\) 或者 \(j = 0\) 时,按情况对 \(\pi_i\) 赋值。

预处理复杂度 \(O(m)\),匹配复杂度 \(O(n)\),整体复杂度 \(O(n+m)\)

Code.
#include<bits/stdc++.h>
#define endl "\n"
#define pb push_back
#define mkp make_pair
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
//value 
const int inf=2147483647;
const int mod=1e9+7;
int cnt[1000005];
char s1[1000005],s2[1000005];


//function 
void solve(){
	
	
	
	return;
}


 
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0); 
	
//	freopen(".in","r",stdin);
//	freopen(".out","w",stdout);
	
	cin>>s1>>s2;
	int n=strlen(s1),m=strlen(s2);
	int tmp=0;
	for(int i=1;i<m;i++){
		if(s2[i]==s2[tmp])cnt[i]=++tmp;
		else {
			while(tmp && s2[i]!=s2[tmp])tmp=cnt[tmp-1];
			if(s2[i]==s2[tmp])cnt[i]=++tmp;
		}
	}
	tmp=0;
	for(int i=0;i<n;i++){
		while(tmp && s1[i]!=s2[tmp])tmp=cnt[tmp-1];
		if(s1[i]==s2[tmp])tmp++;
		if(tmp==m)cout<<i-m+2<<endl;
	}
	for(int i=0;i<m;i++)cout<<cnt[i]<<' ';
	cout<<endl;
	
	
	
	return 0;
}

KMP 应用

好像更多都是对前缀函数的应用

字符串周期

我们定义字符串 \(s\) 存在周期 \(p\) 当且仅当 \(0 < p \le |s|\),若 \(s_i = s_{i+p}\) 对所有 \(i \in [0, |s| - p - 1]\) 成立。

举个例子:字符串 abababab 中,ab、abab、ababab 均为该字符串的周期。

对字符串 \(s\)\(0 \le r < |s|\),若 \(s\) 长度为 \(r\) 的前缀和长度为 \(r\) 的后缀相等,就称 \(s\) 长度为 \(r\) 的前缀是 \(s\) 的 border。

\(s\) 有长度为 \(r\) 的 border 可以推导出 \(|s|-r\)\(s\) 的周期。

根据前缀函数的定义,可以得到 \(s\) 所有的 border 长度,即 \(\pi[n-1],\pi[\pi[n-1]-1], \ldots\)

所以根据前缀函数可以在 \(O(n)\) 的时间内计算出 \(s\) 所有的周期。其中,由于 \(\pi[n-1]\)\(s\) 最长 border 的长度,所以 \(n - \pi[n-1]\)\(s\) 的最小周期。

例题:P3435 [POI 2006] OKR-Periods of Words

(注意到这个题是需要记忆化的)

字符串压缩

(其实感觉这玩意除了针对性的出题不会用到这玩意)

目的是对于字符串 \(s\),找出一个字符串 \(t\) 使得 \(s\) 可以由一份或多份 \(t\) 拷贝而成。

证明过程较为复杂,可以只记住结论(吧):令 \(k = n - \pi_{i-1}\),若 \(k\)\(n\) 的因数,\(t\) 的长度为 \(k\),否则为 \(n\)

拓展 KMP

拓展 KMP,又称 Z 函数,定义 \(z_i\) 表示字符串 \(s\) 和以 \(s_i\) 开头的后缀两个字符串所拥有的最长公共前缀的长度, 则 \(z\) 被称作 Z 函数。

关于这玩意,\(O(n)\) 的解法在,一个非常强劲的大佬,我在这里讲点猎奇的(单 \(\log\) 的)。

我们从定义思考,两个字符串的前缀相同,我们首先想到哈希应该是可以解决的,然后想到这个长度也是具有单调性的,即小于长度时哈希值相同,大于长度是哈希值不同,于是我们就可以用二分哈希在 \(O(n \log n)\) 中解决了。

posted @ 2025-08-14 10:17  -Delete  阅读(6)  评论(0)    收藏  举报