2.3KMP算法

写在前面

喜报:听了四遍都没学懂的KMP算法,终于在 gyy 大佬的耐心讲解下搞懂了,大佬orz!!!
因为有ybt,所以直接在上面写吧

正文

kmp算法本质上就是对模式串(要匹配的子串 两个串中短的那个 )中很多重复的前缀和后缀索引起来,使得在一个地方失配了也不要紧,不用重新来的算法(看不懂不要紧)

下面我们就来详细介绍一下kmp的几个操作


预处理

a b a b c

\(nxt[i]\) 来存储对于模式串1~i中的最长前缀和后缀相等的长度
例如

a b a b c

\(i=3\) 时 前缀 \(a\) 等于后缀 \(a\) 所以 \(nxt[3]=1\)
\(i=4\) 时 前缀 \(ab\) 等于后缀 \(ab\) 所以 \(nxt[4]=2\)
而在 在 \(i=5\) 时 前缀没有等于后缀所以 \(nxt[5]=0\)
你会发现一个问题,其实 \(nxt[i]\) 就是 \(i\) 上一次在模式串出现的位置(那干嘛不叫 last 呢)
那聪明的你一定会问:这个是用来干嘛的呢?
别急,让我们来看适配操作

适配

a b a b e ...
a b a b c

我们用 \(i\) 来表示文本串(被适配的串)的下标,\(j\) 来表示模式串的下标
我们先一项一项的配,当配到 \(j=5\) 时就发现两个串失配了,那怎么办,前面是不是白配了呢?
别担心,你会发现 \(j=5\) 的前一项 \(j=4\) 其实是适配的,于是就联想到我的 \(nxt[4]==2\) 其实如果和此时的 \(i=4\) 来配也是可以适配的,因为在第 \(j\) 项长度为 \(nxt[j]\) 前缀和后缀是相等的
所以我就将 \(j=nxt[j]\)\(j+1\) 项也适配为止,然后就大功告成了!

提示

其实预处理 \(nxt[j]\) 数组就相当于模式串自身匹配,几乎和适配操作一样,不多赘述

复杂度

\(O(N)\) 为什么呢?可能有小朋友就要问了,那个j不是跳了好多次吗?
你考虑j是不是每次都往前跳一格或往后退,它最多前进n次,而后退却是在前进的基础上后退回去的,次数必然小于前进的次数,所以最多给kmp带来 \(O(N)\) 的常数,这就是均摊的想法

代码

#include<bits/stdc++.h>
using namespace std;
const int N=1e6+5;
char a[N],b[N];
int nxt[N];
int main(){
	cin>>a+1>>b+1;
	int la=strlen(a+1),lb=strlen(b+1);
	for(int i=1,j=0;i<=lb;i++){
		while(j&&b[i+1]!=b[j+1])  j=nxt[j]; //如果i+1失配则找到i的上一个相同配子 
		if(b[i+1]==b[j+1])  j++;
		nxt[i+1]=j;//本应该是j+1与i+1匹配但是上面已经j++了
	}
	for(int i=0,j=0;i<=la;i++){
		while(j&&a[i+1]!=b[j+1])  j=nxt[j];
		if(a[i+1]==b[j+1])  j++;
		if(j==lb){
			printf("%d\n",i-lb+2);
		}
	}
	for(int i=1;i<=lb;i++){
		printf("%d ",nxt[i]);
	}
}

T1:

板子

T2:


因为\(n-nxt[n]\) 必然是全字符串最短的并且重复的,所以只要求重复多少次就行

T3:


一就是考虑我们要让Q最大就要让绿色框内的字符最短,就是求出来最短的前缀和后缀相等,所以可以考虑二操作,递归nxt直到找到最小的 \(nxt_{nxt_{nxt_{nxt_{nxt_i}}}}\) 禁止套娃

T4:

枚举每一个左端点跑一边kmp,然后递归到 \(j*2>=i+1\) 为止(为什么是i+1呢,因为此时j对应的前缀与,i+1对应的后缀是相等的,在统计nxt数组时 \(nxt[i+1]=j\) 也是这个道理
还有一个细节,就是为什么j可以直接递归到合法情况而不是再开一个jj去统计,因为考虑在下一层递归时,i的长度只加一,j也是同理,而且也只有满足 \(j*2>=i+1\) 才合法,再开一个jj是等价的,至于第一次 \(if(j*2>=i+1) j=nxt[j];\) 本应该写while,但这里写if也过了的原因也是这个道理

我把这篇代码贴出来方便理解
#include<bits/stdc++.h>
using namespace std;
const int N=2e4;
char s[N];
string a;
int k,n,ans;
int nxt[N];
void kmp(){
	int len=a.length()-1;
	for(int i=1;i<=n;i++){
		nxt[i]=0;
	}
	for(int i=1,j=0;i<=len;i++){
		while(j&&a[i+1]!=a[j+1])  j=nxt[j];
		if(a[i+1]==a[j+1])  j++;
		nxt[i+1]=j;
	}
	for(int i=1,j=0;i<=len;i++){
		while(j&&a[i+1]!=a[j+1])  j=nxt[j];//因为此时j对应的是i+1
		if(a[i+1]==a[j+1])  j++;
//      int jj=j;
//		while(jj*2>=i+1)  jj=nxt[jj];//因为此时j对应的是i+1
//		if(jj>=k)  ans++;
		if(j*2>=i+1)  j=nxt[j];
		if(j>=k)  ans++;
	}
}
int main(){
	scanf("%s%d",s+1,&k);
	n=strlen(s+1);
	for(int i=1;i<=n;i++){
		a.clear();
		a+=' ';
		a+=s+i;
		kmp();
	}
	printf("%d",ans);
}

T5:

枚举一个串的左端点,用它的后缀与其余的串进行匹配,j最大就是最长字串

T6:

kmp自己做出来的题,好耶!
跑一边kmp,j合法后递归jj,直到 \(jj==0\) ,但会超时,记忆化一下,就过了

T7:

很容易想到 \(nxt[i]=i-p[i]\) 那么就是将nxt数组告诉你,然后求字典序最小的串
考虑 \(nxt[i]!=0\) 时只需要让 \(a[i]=a[nxt[i]]\) 就行
如果 \(nxt[i]==0\),分两种情况,首先它不能等于 \('a'\) 其次递归nxt数组,不能让它等于nxt+1

T8:

像这种把匹配字符转化到匹配别的东西的方法很经典,就是预处理每个相同字符的位置,但是会有个问题,就是上一个字符可能不在匹配范围内,所以就将这种改成0就行了
我之前将没出现过的设为0

T9:

和T8一样的思路,我们把其转化为几层限制,设 \(id1,id2,id3\) 分别表示在第 \(i\) 个数出现之前最小的比 \(i\) 大的数的最靠右边的位置,和 \(i\) 相等的数最靠右边的位置,最大的比 \(i\) 小的数最靠左的位置,然后 kmp进行比较

我的思路(假):
我设 \(id1,id2,id3\) 表示在 \(i\) 出现之前最后一个比 \(i\) 大的数,最后一个等于 \(i\) 的数,最后一个小于 \(i\) 的数

90pts,然后唐老师造了一组hack:

3 1 5 4
3 1 5 2

原因:是因为这种只能小范围的相等,只要排列一复杂,限制多了,就会有问题

正解:可以用set+桶来找到离他最近的两个值

posted @ 2024-09-27 09:35  daydreamer_zcxnb  阅读(20)  评论(0编辑  收藏  举报