KMP算法

文章介绍了字符串的border概念及其在求解字符串问题中的应用,如Next表的构建,以及如何使用KMP算法进行子串匹配,包括P3375模板题和加强版问题的解决方案。
摘要由CSDN通过智能技术生成

一、概念

1 Border

一个字符串的border指的是这个字符串的最长公共前后缀,且border的长度不能等于原字符串的长度 。例如:

a b b a b abbab abbab 的border是 a b ab ab

还有一个结论:字符串s的最小循环节长度=|s|-border

2 Next表

N e x t [ i ] Next[i] Next[i] 表示以下标 i i i 为结尾的前缀的border的长度
求解:(对于字符串t)

  1. 如果 t [ n e x t [ i ] + 1 ] = = t [ i + 1 ] t[next[i]+1]==t[i+1] t[next[i]+1]==t[i+1] ,那么 n e x t [ i + 1 ] next[i+1] next[i+1] 显然应该等于 n e x t [ i ] + 1 next[i]+1 next[i]+1
  2. 否则,我们发现前缀next[i]的border,也即前缀next[next[i]],他同时也是前缀next[i]的后缀,而前缀next[i]又是i的后缀,所以前缀next[next[i]]也是前缀i的后缀。

下面是代码:

int nex[N];
string s;
void get_next(string s){
	int i,j;
	for(nex[1]=j=0,i=2;s[i];i++){
		while(j&&s[i]!=s[j+1]){
			j=nex[j];
		}
		if(s[i]==s[j+1]){
			j++;
		}
		nex[i]=j;
	}
}

3 KMP

从1开始比较,发现失配就往后跳。由于失配前全都匹配,所以可以放心大胆的跳。
代码和上面的差别不大:

void KMP(string p,string s){
	ll i,j,k=0;
	for(int j=0,i=1;s[i];i++){
		while(j&&s[i]!=p[j+1]){
			j=nex[j];
		}
		if(s[i]==p[j+1]){
			j++;
		}
		if(!p[j+1]){
			cout<<i-j+1;pr;//这里是找到了一种情况
			j=nex[j];
		}
	}
}

二、实践

1 P3375 【模板】KMP

1 题目描述

现在请你求出 s 2 s_2 s2 s 1 s_1 s1 中所有出现的位置。

定义一个字符串 s s s 的 border 为 s s s 的一个 s s s 本身的子串 t t t,满足 t t t 既是 s s s 的前缀,又是 s s s 的后缀。
对于 s 2 s_2 s2,你还需要求出对于其每个前缀 s ′ s' s 的最长 border t ′ t' t 的长度。

2 思路

直接用上面的代码即可

3 代码

#include<bits/stdc++.h>
#define ll long long
#define bug printf("---OK---")
#define pa printf("A: ")
#define pr printf("\n")
#define pi acos(-1.0)
#define INF 0x3f3f3f3f3f3f3f3f
using namespace std;
const ll N=1e6+2;
ll nex[N];
string s,p;
ll cnt;
void get_next(string s){
	int i,j;
	for(nex[1]=j=0,i=2;s[i];i++){
		while(j&&s[i]!=s[j+1]){
			j=nex[j];
		}
		if(s[i]==s[j+1]){
			j++;
		}
		nex[i]=j;
	}
}
void KMP(string p,string s){
	ll i,j,k=0;
	for(int j=0,i=1;s[i];i++){
		while(j&&s[i]!=p[j+1]){
			j=nex[j];
		}
		if(s[i]==p[j+1]){
			j++;
		}
		if(!p[j+1]){
			cout<<i-j+1;pr;
			j=nex[j];
		}
	}
}
int main(){
	cin>>s>>p;
	s=' '+s;p=' '+p;
	get_next(p);
	KMP(p,s);
	for(int i=1;i<p.size();i++){
		cout<<nex[i]<<" ";
	}
	return 0;
}

2 终曲(加强版)

1 题目描述

给出三个字符串,请说出第一串的某个子串,要求该子串长度最小,并且同时包含第2个串和第3个串。

特别地,如果有多个这样的子串,则请输出字母序最小的一个。

2 思路

使用KMP,计算字符串s中子串p1p2的匹配位置,分别存储在vl1vr1vl2vr2中。
遍历第一个串的所有匹配位置,对于每个匹配位置,使用二分查找在第二个串的匹配位置中找到合适的位置,更新答案的区间。

3 代码

#include<bits/stdc++.h>
#define ll long long
#define bug printf("---OK---")
#define pa printf("A: ")
#define pr printf("\n")
#define pi acos(-1.0)
#define INF 0x3f3f3f3f3f3f3f3f
using namespace std;
const ll N=2e5+8;
ll nex[N];
ll ansl,ansr=INF;
vector<pair<ll,ll> > rg;
char s[N],p1[102],p2[102];
vector<ll> vl1,vr1,vl2,vr2;
string ans;
void getans(string s){
    if(ans.size()==0||ans>s){
    	ans=s;
	}
}
void update(ll l,ll r){
	if(ansr-ansl>r-l){
		ansr=r,ansl=l;
		rg.clear();
		rg.push_back(make_pair(l,r));
	}
	else if(ansr-ansl+1==r-l+1){
		rg.push_back(make_pair(l,r));
	}
}
void get_next(char s[]){
	int i,j;
	nex[i]=0;
	for(int i=2,j=0;s[i];i++){
		while(j&&s[i]!=s[j+1]){
			j=nex[j];
		}
		if(s[i]==s[j+1]){
			j++;
		}
		nex[i]=j;
	}
}
void KMP(char s[],char p[],vector<ll>&vl,vector<ll>&vr){
	ll n=strlen(p+1);
	get_next(p);
	ll i,j,sum=0;
	for(j=0,i=1;s[i];i++){
		while(j&&s[i]!=p[j+1]){
			j=nex[j];
		}
		if(s[i]==p[j+1]){
			j++;
		}
		if(!p[j+1]){
			vl.push_back(i-n+1);
			vr.push_back(i);
			j=nex[j];
		}
	}
}
int main(){
	scanf("%s%s%s",s+1,p1+1,p2+1);
	KMP(s,p1,vl1,vr1);
	KMP(s,p2,vl2,vr2);
	ll n=vl1.size(),m=vl2.size();
	if(n==0||m==0){//简单的优化
		printf("No");
		return 0;
	}
	for(int i=0;i<n;i++){
		//第一个比l1大的r2
		ll idx=lower_bound(vr2.begin(),vr2.end(),vl1[i])-vr2.begin();
		if(idx!=m){
			update(min(vl1[i],vl2[idx]),max(vr1[i],vr2[idx]));
		}
		//第一个比l1小的r2
		idx--;
		if(idx>=0){
			update(min(vl1[i],vl2[idx]),max(vr1[i],vr2[idx]));
		}
		//本来还应有两种情况,但因本题数据较弱,仅用前两种即可。
	}
	ll len=rg.size();
	for(int i=0;i<len;i++){
		char t=s[rg[i].second+1];
		s[rg[i].second+1]='\0';
		getans(string(s+rg[i].first));
		s[rg[i].second+1]=t;
	}
	cout<<ans;
	return 0;
}
  • 5
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值