KMP模式匹配

概念

\(\text{KMP}\) 算法,又称模式匹配算法,能够在 \(O(n+m)\) 的时间复杂度内求解如下的问题:

有两个字符串 \(p[n]\)\(s[m]\) ,称字符串 \(p\) 为模式串、字符串 \(s\) 为文本串。要求判断模式串 \(p\) 是否为文本串 \(s\) 的子串,并求出模式串 \(p\) 在文本串 \(s\) 中出现的位置。

算法流程

  1. 对模式串 \(p\) 进行自我匹配

对于模式串 \(p\)\(i\) 位,求出最大的位数 \(f[i]\) ,使得模式串 \(p\) 中以第 \(i\) 位为结尾且长度为 \(f[i]\) 的子串和模式串 \(p\)\(f[i]\) 位组成的字符串重合。这里的 \(f\) 数组就是模式串 \(p\) 的失配数组。

  1. 扫描一遍文本串 \(s\) ,利用 \(f\) 数组寻找模式串 \(p\)

假设现在文本串 \(s\) 已经匹配到第 \(i\) 位,模式串 \(p\) 已经匹配到第 \(j\) 位,此时会出现以下两种情况:

\(s[i]=p[j]\) ,即当前字符匹配成功,就令 \(i++\)\(j++\) ,继续匹配下一对字符;

\(s[i]≠p[j]\) ,即当前字符匹配失败(也称“失配”),由于模式串 \(p\)\(f[j]\) 位组成的字符串与文本串 \(s\)\(i\) 位前 \(f[j]\) 位组成的字符串依然重合(失配数组 \(f\) 的作用),我们就令 \(j=f[j]\) ,也就相当于把模式串 \(p\) 向右移动了 \(j-f[j]\) 位,重新判断当前的 \(s[i]\) 是否等于 \(p[j]\)

当然,如果已经达到 \(j=n\) 的时候,就说明当前模式串 \(p\) 已经在文本串 \(s\) 中完全匹配,我们便可以记录下答案。

如此下去,便能够求出模式串 \(p\) 在文本串 \(s\) 中所有的位置,由此达到我们的目的。

代码

  1. 求模式串 \(p\) 的失配数组 \(f\)
for(int i=2,j=0;i<=n;i++)
{
	while(j>0&&p[i]!=p[j+1])
	{
		j=f[j];
	}
	if(p[i]==p[j+1])
	{
		j++;
	}
	f[i]=j;
}
  1. 文本串 \(s\) 与模式串 \(p\) 的匹配过程:
for(int i=1,j=0;i<=m;i++)
{
	while(j>0&&s[i]!=p[j+1])
	{
		j=f[j];
	}
	if(s[i]==p[j+1])
	{
		j++;
	}
	if(j==n)
	{
		printf("%d\n",i-n+1);
		j=f[j];
	}
}

典型例题

  1. Luogu P4391 [BOI2009] Radio Transmission 无线传输

\(\text{Solution}\)

这是一道求失配数组 \(f\) 的题,比较基础。

#include<iostream>
#include<cstdio>
#define Re register
using namespace std;

const int SIZE=2000001;

char st[SIZE];
int L,f[SIZE];

int main()
{
	scanf("%d",&L);
	for(Re int i=1;i<=L;i++)
	{
		cin>>st[i];
	}
	for(Re int i=2,j=0;i<=L;i++)
	{
		while(j>0&&st[i]!=st[j+1])
		{
			j=f[j];
		}
		if(st[i]==st[j+1])
		{
			j++;
		}
		f[i]=j;
	}
	printf("%d",L-f[L]);
	return 0;
}
  1. Luogu P3435 [POI2006] OKR-Periods of Words

\(\text{Solution}\)

此题用到失配数组 \(f\) 的性质:对于每个 \(i\) 而言,长度为 \(f[i]\) 的前缀与长度为 \(f[i]\) 的后缀是相等的。

由题意可知,如果 \(i\) 有一个公共的前后缀,其长度为 \(l\) ,那么这个前缀 \(i\) 就有一个周期为 \(i-l\)

于是我们用上失配数组 \(f\) 求解即可。

但是,本题求的是最大的周期,因此我们应该求出最小的 \(l\) 满足题意。

当然,这里可以用“记忆化”的思想来简化时间复杂度。

#include<iostream>
#include<cstdio>
#define Re register
#define LL long long
using namespace std;

const int SIZE=2000001;

char st[SIZE];
int L,f[SIZE];
LL Ans;

int main()
{
	scanf("%d",&L);
	scanf("%s",st+1);
	for(Re int i=2,j=0;i<=L;i++)
	{
		while(j>0&&st[i]!=st[j+1]) j=f[j];
		if(st[i]==st[j+1]) j++;
		f[i]=j;
	}
	for(Re int i=1;i<=L;i++)
	{
		int Now=i;
		while(f[Now]>0) Now=f[Now];
		if(f[i]>0) f[i]=Now;
		Ans+=(i-Now);
	} 
	printf("%lld",Ans);
	return 0;
}
  1. Luogu P1470 [USACO2.3] 最长前缀 Longest Prefix

\(\text{Solution}\)

此题在 \(\text{KMP}\) 算法的基础上加了差分的思想。

对于每一个 \(1≤i≤card(O)\) ,在每次用 \(\text{KMP}\) 算法找到 \(s\)\(O[i]\) 所在的位置 \([L,R]\) 时,就修改一下差分数组。

最后对差分数组求前缀和,大于 \(0\) 即为满足题意的长度,输出最大的长度即可。

#include<iostream>
#include<cstdio>
#include<cstring>
#define Re register
using namespace std;

char opt[301][21],st[300001];
int n,m;
int Len[301];
int f[301];
int dif[300001];

int main()
{
	while(1)
	{
		scanf("%s",opt[++m]+1);
		if(opt[m][1]=='.') break;
		Len[m]=strlen(opt[m]+1);
	}
	m--;
	while(scanf("%s",st+n+1)!=EOF)
	{
		n=strlen(st+1);
	}
	for(Re int k=1;k<=m;k++)
	{
		memset(f,0,sizeof f);
		for(Re int i=2,j=0;i<=Len[k];i++)
		{
			while(j>0&&opt[k][i]!=opt[k][j+1])
			{
				j=f[j];
			}
			if(opt[k][i]==opt[k][j+1])
			{
				j++;
			}
			f[i]=j;
		}
		for(Re int i=1,j=0;i<=n;i++)
		{
			while(j>0&&st[i]!=opt[k][j+1])
			{
				j=f[j];
			}
			if(st[i]==opt[k][j+1])
			{
				j++;
			}
			if(j==Len[k])
			{
				dif[i-Len[k]+1]++;
				dif[i+1]--;
				j=f[j];
			}
		}
	}
	for(Re int i=1;i<=n;i++)
	{
		dif[i]+=dif[i-1];
		if(dif[i]<=0)
		{
			printf("%d",i-1);
			return 0;
		}
	}
	printf("%d",n);
	return 0;
}
  1. P4824 [USACO15FEB] Censoring S

\(\text{Solution}\)

此题还用到了栈 \(\text{stack}\) 的思想。

\(\text{KMP}\) 算法过程中,把遍历 \(S\) 时遍历到的 \(i\) 入栈(本题栈中存的最好是下标)。

当匹配上 \(T\) 时,就把匹配成功的部分出栈,然后 \(j\) 从栈中所能匹配到的最大的位置开始,即 \(j=f[stack.len]\) ,继续做下去。

最后把栈中的元素输出即可。

#include<iostream>
#include<cstdio>
#include<cstring>
#define Re register
using namespace std;

const int SIZE=2000001;

char s[SIZE],t[SIZE];
int n,m,Len;
int f[SIZE];
int Last[SIZE];
int Stack[SIZE];

int main()
{
	scanf("%s",s+1);
	scanf("%s",t+1);
	n=strlen(s+1);
	m=strlen(t+1);
	for(Re int i=2,j=0;i<=m;i++)
	{
		while(j>0&&t[i]!=t[j+1])
		{
			j=f[j];
		}
		if(t[i]==t[j+1])
		{
			j++;
		}
		f[i]=j;
	}
	for(Re int i=1,j=0;i<=n;i++)
	{
		while(j>0&&s[i]!=t[j+1])
		{
			j=f[j];
		}
		if(s[i]==t[j+1])
		{
			j++;
		}
		Last[i]=j;
		Stack[++Len]=i;
		if(j==m)
		{
			Len-=m;
			j=Last[Stack[Len]];
		}
	}
	for(Re int i=1;i<=Len;i++)
	{
		printf("%c",s[Stack[i]]);
	}
	return 0;
}

总结

\(\text{KMP}\) 算法的思想在于把模式串 \(p\) 的失配数组 \(f\)\(O(N)\) 的时间复杂度预处理,并在与文本串 \(s\) 匹配的过程中起到关键性的作用:匹配失败就跳到下一个有可能的位置再匹配。相比较于朴素算法,优化了很多不必要的匹配过程。而这也是 \(\text{KMP}\) 算法中最为重要的思想所在。

posted @ 2020-11-21 15:29  kebingyi  阅读(156)  评论(0编辑  收藏  举报