KMP

KMP

今日2025.10.17

成功又开通了博客园,以后就在这里写了

简介

\(KMP\)\(D.E.Knuth\)\(J.H.Morris\)\(V.R.Pratt\) 提出的,用于解决单模式串与主串匹配问题,是十分优秀且巧妙的算法(事实上,字符串的算法都很巧妙)

问题

给定一个长度为 \(n\) 的主串 \(s\),和一个长度为 \(m\) 的模式串 \(p\)\(n,m≤1e6\),询问 \(p\) 是否为 \(s\) 的子串。

暴力算法:

定义两个指针 \(i,j\),分别指向 \(s,p\)(从下标 \(0\) 开始),每次判断 s[i]==p[j],为真则 i++,j++,否则,i-=j-1,j=0,继续匹配,如果 j==m,则匹配成功。但如果一直到 i==n,即退出循环时还未匹配成功,则 \(p\) 不是 \(s\) 的子串

不难证明,暴力法的复杂度为 \(O(nm)\)

KMP模版代码

考虑优化:我们发现,当 s[i]!=p[j],即失配时,让 i-=j-1,j=0的效率过低,而 \(KMP\) 算法则是通过 \(next\) 数组来提高效率,减少无效的匹配

简略介绍一下,\(next[i]\) 表示以 \(i\) 为结尾的最长公共前后缀,直接给出以下例子:

\(c\) \(z\) \(h\) \(c\) \(z\) \(h\) \(c\) \(z\) \(z\)

\(0\) \(0\) \(0\) \(1\) \(2\) \(3\) \(4\) \(5\) \(0\)

不过多介绍,直接看以下代码:

luogu P3375 【模板】KMP

#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
const int N=1e6+5;
int n,m,nxt[N],f[N];
char s[N],p[N];
void kmp(){
	n=strlen(s+1),m=strlen(p+1);
	int j=0;
	for(int i=2;i<=m;i++){
		while(j&&p[i]!=p[j+1]) j=nxt[j];
		if(p[i]==p[j+1]) j++;
		nxt[i]=j;
	}
	j=0;
	for(int i=1;i<=n;i++){
		while(j==m||j&&s[i]!=p[j+1]) j=nxt[j];
		if(s[i]==p[j+1]) j++;
		f[i]=j;
	}
}
int main(){
	cin>>(s+1)>>(p+1);
	kmp();
	for(int i=1;i<=n;i++) if(f[i]==m) cout<<i-m+1<<endl;
	for(int i=1;i<=m;i++) cout<<nxt[i]<<" ";
	return 0;
}

\(KMP\) 真正重要的是求 \(next\) 数组的思想,以及求出来的 \(next\) 的性质,每道和 \(KMP\) 有关的题,都是和 \(next\) 数组有密切关系的,且性质极其巧妙,需多加练习,这个过程中就会不断对 \(next\) 数组有更深的了解

CF1029A Many Equal Substrings

一道构造题,先输出一遍给出的字符串,然后让指针跳转回 \(next[n]+1\) ,继续往后输出即可,跳转 \(k-1\)

代码:

#include<bits/stdc++.h>
using namespace std;
const int N=55;
int n,k,nxt[N];
char s[N];
int main(){
	cin>>n>>k>>(s+1);
	int j=0;
	for(int i=2;i<=n;i++){
		while(j&&s[i]!=s[j+1]) j=nxt[j];
		if(s[i]==s[j+1]) j++;
		nxt[i]=j;
	}
	j=1;
	for(int i=1;i<=k;i++){
		while(j<=n) cout<<s[j++];
		j=nxt[n]+1;
	}
	return 0;
}

P4391 [BalticOI 2009] Radio Transmission 无线传输

依旧是巧妙的用到了 \(next\) 数组的性质的题,做完这道题,将会对 \(next\) 数组有更深刻的理解。我敢保证,如果你不会,去看题解,一定会直呼 \(WC\)

简介

题目给出了 \(n(n≤1e6)\) 和长度为 \(n\) 的字符串 \(s\),已知 \(s\) 是由一个串 \(s1\) 不断循环构成的,求 \(s1\) 的最小长度(输入保证 \(s1\) 至少循环了 \(2\) 次)

说说思路:

根据题意,\(s\) 一定由 \(s1\) 至少循环两次得到,我们给出下图:

\(s1\)\(s\) 分成了若干段,假设 \(s1\) 就是我们所求的最短串,那么有什么性质?

此时,我们会惊奇的发现:\(next[n]\) 就是代表着了图中深蓝色的前缀和绿色的后缀的长度(最长公共前后缀),蓝色部分或者绿色部分加上 \(s1\) 恰好构成完整的 \(s\) ,所以有 \(len(s1)+next[n]=n\),其中 \(len(s)\) 为要求的 \(s1\) 的长度,\(n\)\(s\) 的长度,那么移项可得:\(len(s1)=n-next[n]\),所以就完了。

代码:

#include<bits/stdc++.h>
using namespace std;
const int N=205,L=2e5+5;
int l[N],nxt[N][12],cnt;
string s,p[N];
bool pos[N][L],f[L];
void getnxt(int k){
	int j=0;
	for(int i=2;i<l[k];i++){
		while(j&&p[k][i]!=p[k][j+1]) j=nxt[k][j];
		if(p[k][i]==p[k][j+1]) j++;
		nxt[k][i]=j;
	}
}
void kmp(int k){
	int j=0;
	getnxt(k);
	for(int i=1;i<s.size();i++){
		while((j==l[k])||j&&s[i]!=p[k][j+1]) j=nxt[k][j];
		if(s[i]==p[k][j+1]) j++;
		if(j==l[k]) pos[k][i]=true;
	}
}
int main(){
	string a;
	while(cin>>a){
		if(a==".") break;
		p[++cnt]='.'+a;
		l[cnt]=a.size();
	}
	s=".";
	while(cin>>a) s+=a;
	for(int i=1;i<=cnt;i++) kmp(i);
	f[0]=true;
	for(int i=1;i<s.size();i++){
		for(int j=1;j<=cnt;j++){
			if(pos[j][i]) f[i]|=f[i-l[j]];
		}
	}
	int ans=0;
	for(int i=0;i<s.size();i++) if(f[i]) ans=max(ans,i);
	cout<<ans;
	return 0;
}

P1470 [IOI 1996 / USACO2.3] 最长前缀 Longest Prefix

简介:

题目给出了一个字符串集合 \(P(元素个数≤200,且最大长度均不超过10)\),和一个字符串 \(s(len≤2e5)\),询问 \(s\) 是否可以只由集合 \(P\) 中的元素构成(不要求 \(P\) 中的所有元素都出现,且可以重复使用 \(P\) 中的元素)

思路:

一道DP题,考虑到 \(P\) 中的元素比较少且长度都很小,我们可以直接让每个元素都在 \(s\) 上跑一遍 \(KMP\)把匹配成功的结尾位置标记,然后进行 \(DP\)

用布尔数组 \(pos[i][j]\) 表示 \(P\) 中第 \(i\) 个元素在 \(s\) 中以 \(s[j]\) 为结尾是否能匹配成功,成功为 \(true\) ,否则为 \(false\)

接着设计 \(DP\),定义布尔数组 \(f[i]\) 表示\(s[i]\) 为结尾的前缀,是否能只由 \(P\) 中的元素构成

转移方程为:

if(pos[j][i]) f[i]|=f[i-l[j]]; 其中 \(j\) 为枚举的 \(P\) 中元素的下标

初始化: f[0]=true;

代码:

#include<bits/stdc++.h>
using namespace std;
const int N=205,L=2e5+5;
int l[N],nxt[N][12],cnt;
string s,p[N];
bool pos[N][L],f[L];
void getnxt(int k){
	int j=0;
	for(int i=2;i<l[k];i++){
		while(j&&p[k][i]!=p[k][j+1]) j=nxt[k][j];
		if(p[k][i]==p[k][j+1]) j++;
		nxt[k][i]=j;
	}
}
void kmp(int k){
	int j=0;
	getnxt(k);
	for(int i=1;i<s.size();i++){
		while((j==l[k])||j&&s[i]!=p[k][j+1]) j=nxt[k][j];
		if(s[i]==p[k][j+1]) j++;
		if(j==l[k]) pos[k][i]=true;
	}
}
int main(){
	string a;
	while(cin>>a){
		if(a==".") break;
		p[++cnt]='.'+a;
		l[cnt]=a.size();
	}
	s=".";
	while(cin>>a) s+=a;
	for(int i=1;i<=cnt;i++) kmp(i);
	f[0]=true;
	for(int i=1;i<s.size();i++){
		for(int j=1;j<=cnt;j++){
			if(pos[j][i]) f[i]|=f[i-l[j]];
		}
	}
	int ans=0;
	for(int i=0;i<s.size();i++) if(f[i]) ans=max(ans,i);
	cout<<ans;
	return 0;
}

目前先写到这里,之后有空在更新

posted @ 2025-10-18 16:12  czh(抽纸盒)  阅读(8)  评论(0)    收藏  举报