KMP模式匹配

概念

( ext{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
",i-n+1);
		j=f[j];
	}
}

典型例题

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

( ext{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

( ext{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

( ext{Solution})

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

对于每一个 (1≤i≤card(O)) ,在每次用 ( ext{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

( ext{Solution})

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

( ext{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;
}

总结

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

原文地址:https://www.cnblogs.com/kebingyi/p/14015825.html