KMP算法总结和例题(来自kuangbin套题)

一、KMP算法介绍:

对于两个字符串,如果我们需要在一个串中找到另一个串的出现数量,有两种方法:

在待匹配串中设置指针i,每匹配完一次模式串则往后移一位,该算法的时间复杂度是O(n*m),空间复杂度O(n+m)。

对于这种方法,我们注意到,很多需要匹配的操作实际上是不必要的,比如模式串abcde,待匹配串abcdeabcde,当匹配完b之后移动到匹配c,是完全没有必要的。

因此,为了避免这种情况,KMP算法应运而生。KMP算法对于一个字符串匹配的时间复杂度是O(n+m),空间复杂度是O(n+m)。

以下是KMP算法的实现过程:

KMP算法是通过对模式串预处理,找出失配之后模式串的匹配指针移动的位置,来减少无意义的匹配。这个预处理产生的数组被称为next数组,也称fail数组(理解fail数组的机制就可以对AC自动机的fail指针有更为深刻的理解),以下统一称为fail数组。

fail[i]的意义:

设字符串的字符序号从1开始计数,i是字符串的前i个字符的前缀和后缀的最长相同字符(前后缀均不包括它本身)。

fail数组的求法:

fail数组通过fail[i-1]的信息求解fail[i],利用模式串自己和自己匹配,求得fail数组。fail[0]表示空字符串的前缀和后缀的相同字符长度,显然没有意义,设为-1。设置指针jkj=-1,k=0j为匹配字符位置指针,k为待匹配字符位置指针。然后,如果fail[i-1]=-1,就意味着k的位置为字符串的第一个字符位置,此时得到fail[i]=0;如果fail[i-1]>-1,即表明k的位置大于字符串的第一个字符位置,此时分两种情况:

一、如果两个指针对应的字符相等,则都可以向右移动一位,同时fail[i]=fail[i-1]+1;(若一直都匹配成功,fail[i-1]=j,然后++j

二、如果两个指针对应的字符不相等,则j就要回溯,回溯到哪里呢,这时就要使用fail已经求出的信息了。假设在这之前均匹配成功,由fail数组的定义,字符串中0~fail[i]-1和i-fail[i]~i-1相等,且fail[i]和i所指的位置一定不相等,所以就要找到可能的更短的前缀与其匹配或者不匹配此时,通过j=fail[j]回溯找到更短的匹配。为什么可以这么做呢?我们知道字符串中0~fail[i]-1i-fail[i]~i-1相等,且此时fail[i]=j,当j回溯到fail[j]时,0~j-1子串仍与回溯前的子串的长度为j的后缀串相同,因此相当于是将待匹配串和模式串(其实都是模式串在自己和自己匹配)的已知的最大相同长度部分对齐,然后再进行匹配。

fail数组求解代码:

注:源码是命名为next数组,C++中next是一个已经被使用的名称,故不能使用next作为数组名。

void getnext(string &str)
{
	memset(nextarray, 0, sizeof(nextarray));
	int j = -1, k = 0;
	nextarray[0] = -1;
	while (k < str.size())
	{
		if (j == -1 || str[j] == str[k])
			nextarray[++k] = ++j;
		else
			j = nextarray[j];
	}
}

KMP算法判断模式串是否出现在待匹配串中:

使用已经求得的fail数组对待匹配串求解,设置指针i为待匹配串中的字符位置,指针j为模式串中的字符位置,如果两个位置的字符相等,则指针均向后移一位,如果不相等,我们已经知道了j个字符相等,j=fail[j]就是相当于将模式串0~fail[j]子串和待匹配串以最大相同长度对齐,然后再继续匹配。

匹配代码:

bool kmp(string &a, string &b)
{
	int i = 0, j = 0, ans = 0;
	while (i < a.size())
	{
		if (j == -1 || a[i] == b[j])
			++i, ++j;
		else
			j = nextarray[j];
		if (j == b.size())
			++ans, j = nextarray[j];
	}
	if (ans)
		return true;
	return false;
}
//返回ans即为出现次数

一些小技巧:

(待补充)

二、例题:

1、HDU1686 https://vjudge.net/problem/HDU-1686

详见:https://blog.csdn.net/Aya_Uchida/article/details/88536050

2、HDU2087 https://vjudge.net/problem/HDU-2087

题意:求不重叠子串个数。

题解:水题,直接套匹配模即可,但是j==b.size()里面的j=nextarray[j]改成j=0

AC代码:

#include 
#include 
#include 
using namespace std;
int nextarray[1005];
void getnext(string &str)
{
	memset(nextarray, 0, sizeof(nextarray));
	int j = -1, k = 0;
	nextarray[0] = -1;
	while (k < str.size())
	{
		if (j == -1 || str[j] == str[k])
			nextarray[++k] = ++j;
		else
			j = nextarray[j];
	}
}
int count(string &a, string &b)
{
	int i = 0, j = 0, ans = 0;
	while (i < a.size())
	{
		if (j == -1 || a[i] == b[j])
			++i, ++j;
		else
			j = nextarray[j];
		if (j == b.size())
			++ans, j = 0;
	}
	return ans;
}
int main()
{
	string a, b;
	while (cin >> a && a != "#")
	{
		cin >> b;
		getnext(b);
		cout << count(a, b) << endl;
	}
	return 0;
}

3、HDU3746 https://vjudge.net/problem/HDU-3746

题意:给你一个字符串,请问在该字符串末尾最少添加多少个字符,可以让这个字符串获得重复循环序列。

题解:fail数组的应用,对于一个字符串,设其长度是len,若fail[len]=0,则表明整个字符串不存在部分重复,直接输出原长,如果len%(len-fail[len])=0,即字符串本身就循环,len-fail[len]是最小周期,输出0,其他的,输出len-fail[len]-len%(len-fail[len])。即需要补充以达到字符串的最小完整周期的长度。

AC代码:

#include 
#include 
#include 
using namespace std;
int nextarray[100005];
void getnext(string &str)
{
	memset(nextarray, 0, sizeof(nextarray));
	int j = -1, k = 0;
	nextarray[0] = -1;
	while (k < str.size())
	{
		if (j == -1 || str[j] == str[k])
			nextarray[++k] = ++j;
		else
			j = nextarray[j];
	}
}
void solve(string &str)
{
	getnext(str);
	int cir = str.size() - nextarray[str.size()];
	if (nextarray[str.size()] == 0)
		cout << str.size() << endl;
	else if (str.size() % cir == 0)
		cout << 0 << endl;
	else
		cout << cir - str.size() % cir << endl;
}
int main()
{
	int n;
	cin >> n;
	string a;
	while (n--)
	{
		cin >> a;
		solve(a);
	}
	return 0;
}

4、HDU1358 https://vjudge.net/problem/HDU-1358

题意:给出一个字符串,输出其所有循环节大于1的循环子串的串长度和循环节数量。

题解:求出fail数组之后遍历计算即可。

AC代码:

#include 
#include 
#include 
using namespace std;
int nextarray[1000005];
void getnext(string &str)
{
	memset(nextarray, 0, sizeof(nextarray));
	int j = -1, k = 0;
	nextarray[0] = -1;
	while (k < str.size())
	{
		if (j == -1 || str[j] == str[k])
			nextarray[++k] = ++j;
		else
			j = nextarray[j];
	}
}
void solve(string &str, int kase)
{
	cout << "Test case #" << kase << endl;
	for (int i = 2; i <= str.size(); ++i)
	{
		if (nextarray[i] && i % (i - nextarray[i]) == 0)
			cout << i << " " << i / (i - nextarray[i]) << endl;
	}
	cout << endl;
}
int main()
{
	string a;
	int n, kase = 0;
	while (cin >> n && n)
	{
		cin >> a;
		getnext(a);
		solve(a, ++kase);
	}
	return 0;
}

5、POJ 2752 https://vjudge.net/problem/POJ-2752

详见:https://blog.csdn.net/Aya_Uchida/article/details/88559426

6、UVA 455 https://vjudge.net/problem/UVA-455

详见:https://blog.csdn.net/Aya_Uchida/article/details/88372228

7、洛谷P3435 https://www.luogu.org/problemnew/show/P3435

详见:https://blog.csdn.net/Aya_Uchida/article/details/89187496

(待补充)

三、本人蒟蒻,请大佬多多指教。

你可能感兴趣的:(ACM,算法总结)