KMP算法相关

2020.3.19编辑:
看到当年写的拙劣理解实在是不堪入目。。。虽然不是错漏百出,但也难免误人子弟…
如果有同学看到这里,请牢记理解kmp最关键的两点:

  1. 画图理解,使用可以区分正反顺序的字符串(而不是简单的一个字母)来代表相等的部分
  2. 关于正确性,结合border理论可以自然地反证。

KMP

字符串匹配。
给出一个主串s和另一个串f,问f是否s的子串。

暴力的话最坏复杂度要到O(mn),而用KMP的话通过一个O(m)的预处理就可以O(n)的 处理出结果。
因为只需要处理子串f,所以特别适合于给出一大堆s串,叫你求子串的问题(公共子串问题O( n 2 m n^2m n2m))

主要思想

它利用之前已经部分匹配这个有效信息,假如已经在下一个位置匹配失败,那就不用移动原先起点再重新匹配,直接让f串后移,令f串中尽量多字符在已匹配部分中某个位置 到 已匹配部分的最后一个位置 完整匹配,然后从上次匹配失败的位置上继续匹配。

匹配部分

这里写图片描述
恩,首先是有子串f和主串s,黑色部分是已经匹配成功了的部分。
但下一个字符f和s就不匹配了,这时候,暴力的做法是将f重置,从这一次起点的下一个位置再进行匹配。但KMP的做法是,找出原黑色部分的最大相等前后缀,然后将前缀移动到后缀的位置,再继续匹配。如图所示,其中黑色部分是不需要重新匹配的相同部分,直接从黑色的下一个位置开始匹配。
这里写图片描述

为什么要取最长的前后缀呢?
假如选择一个长度较小的相等前后缀,那显然我们s中起点移动的步数k就会比选择最长相等前后缀的k要大,就会直接略掉最大的这种可能,但没准正确匹配就是从选择最大的开始呢?

CODE

j = 0
i = -1
for (; j < m; j++) {
        while (N[i+1] != M[j] && i >= 0) i = max[i]; //如果不匹配,那就回到最大前后缀中前缀的结尾位置。
        if (N[i+1] == M[j]) i++  //如果匹配就黑色部分长度+1  
        if (i == n-1) {match.push(j-i); i = max[i]}  //继续匹配
    }

最长相等前后缀的计算

定义

max(m)指f的前m个字符中最长相等前后缀

max函数求法

这里写图片描述
假设我们已经知道了max[i-1]的值,前缀=后缀,现在要在原来的基础上再加第i个位置f[i]。
若f[max[i-1]+1]=f[i],那显然就直接拼上去,max[i]=max[i-1]+1;
那要是不等于呢!?
那合并后的新前缀肯定是从原前缀中再取一个前缀+f[i]
同理,新后缀肯定是从原后缀中取一个后缀再+f[i]。

那我们再对原前后缀做一个最大相等前后缀,然后取原前缀的前缀和原后缀的后缀,显然他们是相同的。

那又为什么要长度最长呢? 与前面同理。

即这样
这里写图片描述
图打错了,next[i-1]替换为max[i-1].
然后就判断一下新前缀的下一个是否等于f[i],如果不等那么在新前后缀的基础上重复上述分割步骤,直到长度为0。
j:=max[i-1];//即原前缀最后位置
while (j>0)and(f[i]<>f[j+1]) do j:=max[j]{将原前缀变为新前缀};
其实这样就已经包括了

f[max[i-1]+1]=f[i],那显然就直接拼上去,max[i]=max[i-1]+1;

的情况了,所以删去那一步。
好了,现在我们就愉快的得出了max数组的值

CODE

	i = -1  
    for (; j < n; j++) {
        i = next[j-1]
        while (N[i+1] != N[j] && i >= 0) i = next[i]
        if (N[i+1] == N[j]) next[j] = i+1     
        else next[j] = -1                    
    }

有没有感觉和上面匹配部分的代码神似? 其实就是一个自我匹配的过程。

时间复杂度分析

分析求max的那一段,流程为:

for i=2…m
… 如果当前位置无法与f串下一位置匹配,则将f串位置后移;
… 如果现在可以匹配了就将f已匹配位置+1
… 判断是否匹配完毕

显然在整个循环中,已匹配位置最多增加m-1次(因为只有m-1次循环啊笨)
那每一次while将f串位置后移,已匹配位置最少也会减去1 (aaaaa的情况,一次后移一位)
那已匹配位置不就是最多减去m-1次1,也就是while循环最多执行m-1次。
所以这整个for循环的的时间复杂度就是m-1次for循环+m-1次while循环也就是O(m)。
匹配部分也同理,最多O(n),所以整个时间复杂度就是O(n+m)

完整CODE

var
    p,s:ansistring;
    f:array[0..5000] of longint;
    i,j,k:longint;
procedure getf;
var j:longint;
begin
    f[0]:=-1;
    f[1]:=0;j:=0; i:=1;
    for i:=2 to length(p) do
    begin
        while (j>0)and(p[i]<>p[j+1]) do j:=f[j];
        if (p[i]=p[j+1]) then inc(j);
        f[i]:=j;
    end;

end;

begin
    readln(p); readln(s);
    getf();
    j:=0;
    for i:=1 to length(s) do
    begin
        while (p[j+1]<>s[i])and(j>0) do j:=f[j];
        if p[j+1]=s[i] then inc(j);
        if j=length(p) then
        begin
            writeln(i-j+1);
            break;
        end;
    end;

end.

扩展kmp

e x t [ i ] ext[i] ext[i]为主串S与子串T中,S的后缀s[i,n]与T的最长前缀长度。

目的

求出ext[1…n]

需要用到的内容

nxt[i]表示,子串T本身与子串T的一个后缀t[i…n]的最长前缀长度。

思想

与kmp大致相同,先通过自我匹配求出nxt,再去求ext,建议前置manacher。

具体算法

顺序求解每一个 e x t [ i ] ext[i] ext[i],对于当前的i,我们求一个 m x = m a x ( i + e x t [ i ] − 1 ) mx=max(i+ext[i]-1) mx=max(i+ext[i]1),也就是s串中最靠后的匹配成功位置,设这个位置是从x开始匹配达到的。
然后,因为 s [ x . . m x ] s[x..mx] s[x..mx]这一段与 t [ 1.. e x t [ x ] ] t[1..ext[x]] t[1..ext[x]]匹配,
那么 s [ i . . m x ] s[i..mx] s[i..mx]这一段也就是 t [ i − x + 1.. e x t [ x ] ] t[i-x+1..ext[x]] t[ix+1..ext[x]]
(读者可以画个图理解一下,这里就不放图了)
那么,我们只需要知道 n x t [ i − x + 1 ] nxt[i-x+1] nxt[ix+1],也就是 t [ 1.. l e n t ] t[1..lent] t[1..lent] t [ i − x + 1.. l e n t ] t[i-x+1..lent] t[ix+1..lent]的公共前缀长度,
就有 e x t [ i ] = n x t [ i − x + 1 ] . ext[i]=nxt[i-x+1]. ext[i]=nxt[ix+1].
其意义也就是, s [ i . . m x ] 与 t [ 1.. n x t [ i − x + 1 ] ] s[i..mx]与t[1..nxt[i-x+1]] s[i..mx]t[1..nxt[ix+1]]匹配。

但还有一种情况,也就是nxt[i-x+1]是满的。那么我们无法得知,在S串的mx位置与T串的nxt[i-x+1]位置后,是否能继续匹配。那么我们可以暴力匹配并更新mx,这样保证mx是递增的,所以最终的时间复杂度是 O ( l e n s ) O(lens) O(lens)

对于nxt的求解,与上述匹配方法类似,所以只作简单的流程叙述。
首先求出nxt[1…i-1],现在求解nxt[i],同样记最大匹配位置为mx,达到这个匹配的位置是x
那么i…mx这一段与i-x+1…nxt[x]是相同的。那么同理我们有nxt[i-x+1]=nxt[i]了。不要忘记扩展与更新mx.

#include <cstdio>
#include <iostream>
#include <cstring>
#define min(a,b) ((a)>(b)?(b):(a))
#define maxn 1000010
using namespace std;
int next[maxn],bz[maxn],n,mx,mxf,mans,fr;
char c[maxn];
void out() {
	for (int i=1; i<=n; i++) {
		if (next[i]==0) continue;
		for (int j=1; j<=next[i]; j++) cout<<c[j];
		cout<<endl;
		for (int j=1; j<=next[i]; j++) cout<<c[i+j-1];
		cout<<endl<<endl;
	}
}
int main() {
	scanf("%s",c+1); n=strlen(c+1);
	for (int i=2; i<=n; i++) {
		if (c[i]!=c[i-1]) break;
		next[2]=i-1;
	}
	bz[next[2]]=1;
	next[1]=n;
	mxf=2;
	mx=1+next[2];
	for (int i=3; i<=n; i++) {
		if (i>mx) {
			for (int j=1; j<=n; j++) {
				if (c[i+j-1]!=c[j]) break;
				next[i]=j;
				if (i+next[i]-1!=n) bz[next[i]]=1;
			}
			mx=i+next[i]-1;
			mxf=i;
		} else {
			int p=i-mxf+1; 
			next[i]=min(next[p],next[mxf]-p+1);
			if (i+next[i]-1!=n) bz[next[i]]=1;
			if (next[p]==mx) {
				for (int j=next[i]+1; j<=n; j++) {
					if (c[j]!=c[i+j-1]) break;
					next[i]++;
					if (i+next[i]-1!=n) bz[next[i]]=1;
				}
				mx=i+next[i]-1;
				mxf=i;
			}
		}
	}
	//out();
	//for (int i=1; i<=n; i++) if (i+next[i]-1!=n) bz[next[i]]=1,(cout<<next[i]<<endl);
	bz[0]=0;
	for (int i=1; i<=n; i++) {
		//cout<<i<<" "<<i+next[i]-1<<" "<<bz[next[i]]<<endl;
		if (i+next[i]-1==n && bz[next[i]]) mans=max(mans,next[i]);
	}
	if (mans==0) {
		cout<<"CD can't remember anything."<<endl;
		return 0;
	}
	for (int i=1; i<=mans; i++) printf("%c",c[i]);
} 
  • 4
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值