【超好懂的字符串算法笔记】KMP及其拓展——字符串匹配问题


title : KMP算法及扩展
date : 2021-8-27
tags : ACM,字符串
author : Linno


在这里插入图片描述

简介

KMP算法主要解决子串在母串中的匹配定位(pattern)问题。

可以在O(n+m)的复杂度下找到匹配。

思考

为什么要用kmp

对于在长度为n的母串搜索长度为m的子串问题,双指针暴力枚举的时间复杂度可以达到 O ( n m ) O(nm) O(nm),事实上,指针一格格走会徒增很多没必要的计算。失配时,如果我们能将子串中当前匹配的字符移动到上一次出现的位置,那么就可以减少很多计算。

如何记录需要移动的位置

我们要移动的下一个位置满足这个性质:最前面的k个字符和j之前的最后k个字符是一样的。假设我们在k位置失配,那么移动的位数就和 前 缀 = 后 缀 的 最 大 位 数 前缀=后缀的最大位数 =有关。

板子

//luoguP3375 【模板】KMP字符串匹配
#include<bits/stdc++.h>
using namespace std;
string s1,s2;
int j=0,len1,len2,next[1000007];
signed main(){
	cin>>s1;cin>>s2;
	len1=s1.length();
	len2=s2.length();
	next[0]=0;
	for(int i=1;i<len2;i++){ //获取失配数组
		j=next[i];
		while(j&&s2[j]!=s2[i]) j=next[j];
		next[i+1]=(s2[j]==s2[i])?j+1:0; 
	}
	j=0; 
	for(int i=0;i<len1;i++){
		while(j&&s1[i]!=s2[j]) j=next[j]; 
		if(s1[i]==s2[j]){ 
			j++;
			if(j==len2) cout<<i-j+2<<endl;
		}
	}
	for(int i=1;i<=len2;i++) cout<<next[i]<<" ";
	return 0;
}

前缀函数

vector<int> prefix_function(string s) {
  int n = (int)s.length();
  vector<int> pi(n);
  for (int i = 1; i < n; i++) {
    int j = pi[i - 1];
    while (j > 0 && s[i] != s[j]) j = pi[j - 1];
    if (s[i] == s[j]) j++;
    pi[i] = j;
  }
  return pi;
}

Z函数

定义

对于长度为n的字符串s,定义函数z[i]表示s和s[i,n-1](即以s[i]开头的后缀)的最长公共前缀(LCP,longest common prefix)的长度。特别的,z[0]=0。

Z Box

Z Box是字符串str中的一个区间[l,r]满足str[l,r]是str的前缀(不要求最长)。在位置i时,要求[l,r]必须包含i,且r尽可能大,那么就能得到Z函数数组。计算中我们维护右端点最靠右的Z-Box,规定初始时 l = r = 0 l=r=0 l=r=0,保证 l ≤ i l\leq i li

计算过程

如 果 i ≤ r , 有 s [ i , r ] = s [ i − l , r − l ] , 因 此 z [ i ] ≥ m i n ( z [ i − l ] , r − i + 1 ) 。 ① 若 z [ i − l ] < r − i + 1 , 则 z [ i ] = z [ i − l ] ② 若 z [ i − l ] ≥ r − i + 1 , 令 z [ i ] = r − i + 1 , 然 后 暴 力 枚 举 直 到 z [ i ] 不 能 扩 展 为 止 。 如 果 i > r , 根 据 朴 素 算 法 , 从 s [ i ] 开 始 暴 力 求 出 z [ i ] 在 求 出 z [ i ] 后 , 如 果 i + z [ i ] − 1 > r , 就 需 要 更 新 [ l , r ] , 即 令 l = i , r = i + z [ i ] − 1 。 如果i\leq r,有s[i,r]=s[i-l,r-l],因此z[i]\ge min(z[i-l],r-i+1)。\\ ①若z[i-l]<r-i+1,则z[i]=z[i-l]\\ ②若z[i-l]\ge r-i+1,令z[i]=r-i+1,然后暴力枚举直到z[i]不能扩展为止。\\ 如果i>r,根据朴素算法,从s[i]开始暴力求出z[i]\\ 在求出z[i]后,如果i+z[i]-1>r,就需要更新[l,r],即令l=i,r=i+z[i]-1。 irs[i,r]=s[il,rl]z[i]min(z[il],ri+1)z[il]<ri+1,z[i]=z[il]z[il]ri+1z[i]=ri+1,z[i]i>r,s[i]z[i]z[i]i+z[i]1>r[l,r]l=i,r=i+z[i]1

void getz(int n,char *str){ //Z函数模板
	int l=0,r=0;
	z[0]=0;z[1]=n;
	for(int i=2;i<=n;i++){
		if(i>r){
			while(str[i+z[i]]==str[1+z[i]]) z[i]++;
			l=i,r=i+z[i]-1;
		}else if(z[i-l+1]<r-i+1) z[i]=z[i-l+1];
		else{
			z[i]=r-i;
			while(str[i+z[i]]==str[1+z[i]]) z[i]++;
			l=i,r=i+z[i]-1;
		}
	}
}

扩展KMP

问题描述

给定两个字符串str1,str2,求出str1的每一个后缀与str2的最长公共前缀。

Ext Box

当str1==str2时,就相当于求z函数。

当str1和str2不相同时,用ext[i]表示str1第i位开始的后缀与str2的最长公共前缀,只需要修改z函数的推导过程即可。

luoguP5410 【模板】扩展 KMP(Z 函数)
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e7+7;
int n,m,ans1,ans2;
int z[N],ext[N];
char str1[N],str2[N];

void getz(){ //str2的每一个后缀与str2的最长公共前缀
	z[0]=m;
	int l=0,r=0;
	for(int i=1;i<m;i++){ //和exkmp的求法是一样的,这个写法好像比较简单 
		if(i<=r&&z[i-l]<r-i+1) z[i]=z[i-l];
		else{
			z[i]=max(0ll,r-i+1);
			while(i+z[i]<m&&str2[i+z[i]]==str2[z[i]]) z[i]++;
		}
		if(i+z[i]-1>r) l=i,r=i+z[i]-1;
	}
}

void exkmp(){ //str1的每一个后缀与str2的最长公共前缀
	int now=0;
	while(str1[now]==str2[now]&&now<min(n,m)) now++; //暴力
	ext[0]=now;
	int p=0;
	for(int i=1;i<n;i++){
		if(i+z[i-p]<ext[p]+p) ext[i]=z[i-p];
		else{
			int now=ext[p]+p-i;
			now=max(now,0ll);
			while(str2[now]==str1[i+now]&&now<m&&now+i<n) now++;
			ext[i]=now;
			p=i;
		} 
	} 
}

signed main(){
	scanf("%s%s",str1,str2);
	n=strlen(str1),m=strlen(str2);
	getz();
	exkmp();
	/*
	for(int i=0;i<m;i++) cout<<z[i]<<" ";
	cout<<endl;
	for(int i=0;i<n;i++) cout<<ext[i]<<" ";
	cout<<endl;
	*/ 
	for(int i=0;i<m;i++) ans1^=(i+1)*(z[i]+1);
	for(int i=0;i<n;i++) ans2^=(i+1)*(ext[i]+1);
	cout<<ans1<<"\n"<<ans2<<"\n"; 
	return 0;
}

参考

OI-wiki

https://www.bilibili.com/video/BV1LK4y1X74N

https://www.luogu.com.cn/blog/pks-LOVING/zi-fu-chuan-xue-xi-bi-ji-qian-xi-kmp-xuan-xue-di-dan-mu-shi-chuan-pi-post

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
KMP算法是一种字符串匹配算法,用于在一个文本串S内查找一个模式串P的出现位置。它的时间复杂度为O(n+m),其中n为文本串的长度,m为模式串的长度。 KMP算法的核心思想是利用已知信息来避免不必要的字符比较。具体来说,它维护一个next数组,其中next[i]表示当第i个字符匹配失败时,下一次匹配应该从模式串的第next[i]个字符开始。 我们可以通过一个简单的例子来理解KMP算法的思想。假设文本串为S="ababababca",模式串为P="abababca",我们想要在S中查找P的出现位置。 首先,我们可以将P的每个前缀和后缀进行比较,得到next数组: | i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | | --- | - | - | - | - | - | - | - | - | | P | a | b | a | b | a | b | c | a | | next| 0 | 0 | 1 | 2 | 3 | 4 | 0 | 1 | 接下来,我们从S的第一个字符开始匹配P。当S的第七个字符和P的第七个字符匹配失败时,我们可以利用next[6]=4,将P向右移动4个字符,使得P的第五个字符与S的第七个字符对齐。此时,我们可以发现P的前五个字符和S的前五个字符已经匹配成功了。因此,我们可以继续从S的第六个字符开始匹配P。 当S的第十个字符和P的第八个字符匹配失败时,我们可以利用next[7]=1,将P向右移动一个字符,使得P的第一个字符和S的第十个字符对齐。此时,我们可以发现P的前一个字符和S的第十个字符已经匹配成功了。因此,我们可以继续从S的第十一个字符开始匹配P。 最终,我们可以发现P出现在S的第二个位置。 下面是KMP算法C++代码实现:

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

RWLinno

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值