字符串的匹配[kmp]

KMP算法是一种用于字符串匹配的高效算法,通过next数组优化了失配后的回溯过程。文章详细介绍了KMP算法的原理,包括next数组的生成和失配传送的处理,并给出了一个简单的程序示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

kmp问题

·>kmp问题是指在一个长字符串中寻找能和已知字符串匹配位数的问题。

  • kmp算法的原理
    • next数组\kmp数组
  • kmp算法的实现
    • next数组\kmp数组的实现
    • 失配传送的实现
  • 实例

【kmp算法的原理】

**引例:**给出两个字符串s1和s2,其中s2为s1的子串,求出s2在s1中所有出现的位置。

输入样例#1输出样例#1
ABABABC1
ABA3
0 0 1

*首先从朴素算法开始。我们会先将s1的第一位和s2的第一位进行比较,发现A与A匹配,然后比较s1 & s2第二位……直到发现s1 & s2的第三位A与A相等,我们输出第一次的位置1,然后返回到s2的第一位,和s1的第二位进行比较。到现在我们可以发现了,朴素算法的缺点就在这里:每一次失配后或者完成匹配后都需要返回到最初的位置的下一个位置。而kmp问题就是优化了这个步骤。
*kmp算法通过使用next[]数组(或者是kmp[]数组)来记录在s2的某一个位置失配以后可以跳回到前面的哪一位,然后在失配后调用数组跳回达到速度优化。

[next数组\kmp数组]

这里引用一个视频 (阿三讲的真实好)

简单来说,kmp数组就是将待匹配字符串s2中进行前缀和后缀的匹配。由于在s1失配位的字符之前的一段字符都是和相对应的s2中的一部分字符串匹配的,所以只要寻找s1中在这一位之前的字符串中是否存在一段相等的字符,例如ABCABTTTTT中,如果失配位置是第一个T,那么在T之前的字符串中可以发现B与B匹配,AB与AB匹配,所以在执行跳回的过程中有这样两个选择,从s2的第一B开始重新匹配和从s2的第一个AB开始匹配。但是我们可以发现从AB开始匹配显然是最优解,所以我们这里定义的next[]数组记录的就是失配以后可以跳回的最末的前缀位置的下一位。

|0|1|2|3|4|5|

|A|B|C|A|B|T|


【kmp算法的实现】

[next数组\kmp数组的实现]
for(int i=1;i<len2;i++){//i从1向末移动
        while(s2[i]!=s2[j]&&j) j=next[j];
        //s2[i]!=s2[j]表示如果现在已知的最长前缀末位置上的字符与索引字符i不匹配,
        //那么就不断将可以使用的最长前缀末位置传送到从s2[1]~s2[j-1]的字符串中可
        //以匹配的一对最大前后缀中的最大前缀的末位置,或者找不到可以匹配的前后缀
        //则直到j变为0,遍历完全。
        if(s2[j]==s2[i]){
        //判断当前索引字符与经过上面一步处理后的结果到底是前一种得到匹配的可能还
        //是后一种没有得到匹配的可能
            next[i+1]=++j;
            //这种就是前一种得到匹配的可能,这时候最大匹配前缀末位后一位已经变成
            //最大匹配前缀末尾,将j+1以后移动到新的最大匹配前缀末尾,并且将他附
            //加给next数组
        }else{
            next[i+1]=0;
            //这种就是后一种没有得到匹配的可能,这时候由于失配只能回到0
        }
        //上面一步是对第一个while的结果判断和补充,如果执行else则k已经是0了
    }
    //创建next[]结束
[失配传送的实现]
for(int i=0;i<len1;i++){
        //cout<<"i="<<i<<"j="<<j<<endl; 
        while(s2[j]!=s1[i]&&j) j=next[j];
        //s2[j]!=s1[i]表示如果现在待匹配字符和予匹配字符不符合的话就传送到前一
        //个可以匹配的最大前缀处,要么找到后达到可以匹配的最大前缀后一位,要么
        //传送到匹配源串的头 
        if(s1[i]==s2[j]){
            j++;//如果成功匹配这一位的话就将源串指针向后移,用来适应for循环中i
            //指针的移动 
        }
        if(j==len2){
            cout<<i-len2+2<<endl;
            //如果检测到源串指针达到源串最后一位,则得到一个满足的位置。 
        }
    }
[程序]
#include<map>
#include<set>
#include<cmath>
#include<queue>
#include<vector>
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<iostream>
#include<algorithm>
#define rep(i,a,b) for(register int i = (a);i <= (b);++i)
#define per(i,a,b) for(register int i = (a);i >= (b);--i)  
typedef long long ll;
typedef unsigned long long ull;
using std::string;using std::cin;using std::cout;

char str[1000010],m[1000010];
int len_str,len_m;
int kmp[1000010];

int main(){
    std::ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    //freopen("in.in", "r", stdin);
    cin >> m+1 >> str+1;
    len_m = strlen(m+1) , len_str = strlen(str+1);
    int now = 0;
    rep(i,2,len_str){
        while(now && i <= len_str && str[now+1] != str[i]) now = kmp[now];
        if(str[now+1] == str[i]) kmp[i] = ++now;
    }
    now = 0;
    rep(i,1,len_m){
        while(now && i <= len_m && str[now+1] != m[i]) now = kmp[now];
        if(str[now+1] == m[i]) ++now;
        if(now == len_str){
            cout << i - now + 1 << '\n';
        }
    }
    rep(i,1,len_str) cout << kmp[i] << " ";
    return 0;
}```

### 【实例】

#### P4391 [BOI2009]Radio Transmission 无线传输
```cpp
#include<iostream>
#include<cstring>
using namespace std;
char s2[2000001];
int next[2000001];
//就是kmp数组,用来记录到此为止匹配出错时可以与下标之前的后缀匹配的最长前缀末位
//置的后一个,下一次直接从赋值下一个位置开始匹配
int main(){
    int i,j,n;//i用来在进行匹配任务的字符串中进行索引,从1向末移动
    j=0;
    cin>>n;
    cin>>s2;//s1的长度
    for(int i=1;i<n;i++){//i从1向末移动
        while(s2[i]!=s2[j]&&j) j=next[j];
        //s2[i]!=s2[j]表示如果现在已知的最长前缀末位置上的字符与索引字符i不匹配,
        //那么就不断将可以使用的最长前缀末位置传送到从s2[1]~s2[j-1]的字符串中可
        //以匹配的一对最大前后缀中的最大前缀的末位置,或者找不到可以匹配的前后缀
        //则直到j变为0,遍历完全。
        if(s2[j]==s2[i]){
        //判断当前索引字符与经过上面一步处理后的结果到底是前一种得到匹配的可能还
        //是后一种没有得到匹配的可能
            next[i+1]=++j;
            //这种就是前一种得到匹配的可能,这时候最大匹配前缀末位后一位已经变成
            //最大匹配前缀末尾,将j+1以后移动到新的最大匹配前缀末尾,并且将他附
            //加给next数组
        }else{
            next[i+1]=0;
            //这种就是后一种没有得到匹配的可能,这时候由于失配只能回到0
        }
        //上面一步是对第一个while的结果判断和补充,如果执行else则k已经是0了
    }
	cout<<n-next[n];
}

过于慵懒甚至直接复制

P4173 残缺的字符串(这个只有80分)

本来觉得这道题真jier简单,居然是省选,结果越想越难受一做分能骗但a不了(哭了,看了题解发现尽是FFT??!!那好吧,我就把能骗到分的程序来了。

# include<iostream>
# include<cstring>
# include<cstdio>
using namespace std;
int ans[3000001];
string a,b;
int main(){
	int l1,l2;
	cin>>l1>>l2>>a>>b;
	for(int i=0;i<=l2-l1;++i)
	  {
	  	bool fl=1;
	  	int j=i,l=0;
	  	while(l<l1)
	  	if(a[l]==b[j]||a[l]=='*'||b[j]=='*') j++,l++;
	  	else
	  	{
	  		fl=0;
	  		break;
		}
		if(fl) ans[++ans[0]]=i+1;
	  }
	cout<<ans[0]<<endl;
	for(int i=1;i<=ans[0];++i)
		cout<<ans[i]<<" ";
	return 0;
}
### KMP算法的实现与原理 KMP(Knuth-Morris-Pratt)算法是一种高效的字符串匹配算法,它通过预先处理模式串的部分匹配信息,在主串中寻找子串的过程中减少不必要的字符比较。以下是关于该算法的核心概念及其具体实现。 #### 部分匹配表(Next数组) 部分匹配表是KMP算法的关键所在。它的作用在于记录模式串前缀和后缀的最大重合长度。这种特性使得在发生失配时,可以直接跳转到已知位置继续匹配而无需重新开始[^2]。 对于给定的模式串`P`,可以通过以下方式计算得到对应的`next[]`数组: ```python def compute_next_array(pattern): next_arr = [0] * len(pattern) j = 0 # 前缀指针 for i in range(1, len(pattern)): while j > 0 and pattern[i] != pattern[j]: j = next_arr[j - 1] if pattern[i] == pattern[j]: j += 1 next_arr[i] = j return next_arr ``` 此函数实现了基于当前索引`i`处字符的状态更新逻辑,并最终返回完整的`next[]`数组[^1]。 #### 主串扫描过程 一旦获得了`next[]`数组之后,就可以将其应用于实际的字符串匹配流程当中去了。下面展示了一个标准版本的KMP搜索方法: ```python def kmp_search(text, pattern): n = len(text) m = len(pattern) next_arr = compute_next_array(pattern) matches = [] q = 0 # 当前状态/模式串中的位置 for i in range(n): while q > 0 and text[i] != pattern[q]: q = next_arr[q - 1] if text[i] == pattern[q]: q += 1 if q == m: matches.append(i - m + 1) q = next_arr[m - 1] return matches ``` 这里定义了两个主要变量:一个是代表文本流迭代器的外层循环;另一个则是跟踪模式串进展程度的内部计数器。每当遇到相等项,则推进一步直至完成整个序列或者发现新的冲突为止[^3]。 #### 时间复杂度分析 由于每次失败后的调整都依赖于先前积累的知识而非简单地退回起点,因此理论上讲,最坏情况下的运行时间为O(N),其中N为主串长度。这相比传统的朴素做法具有显著优势,尤其是在面对大量重复数据结构的情况下更是如此[^4]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值