KMP算法(字符串匹配问题)acm寒假集训日记22/1/19

首先,先看一道例题:

如果不考虑超时的话,我们完全可以用最朴素的方法(暴力)去求

//暴力算法(n*m) 
int ViolentMatch(char *s,char *p)
{
	int sLen = strlen(s);
	int pLen = strlen(p);
	
	int i = 0;
	int j = 0;
	while(i<sLen&&j<pLen)
	{
		if(s[i]==p[j])
		{
			i++;
			j++;
		}
		else
		{
			i = i-j+1;
			j = 0;
		}
	}
	if(j==pLen)
	return i-j;
	else
	return -1;
}

但n*m的时间复杂度显然是不行的(1e7~1e8——》1s)

这就引进了一个问题:如何优化?

我们不难发现:朴素的算法最浪费时间的是——》一个一个回溯的过程(找到不匹配的,j = 0,i = i-j+1)。简单来说就是i不断的回溯,很多情况下是没有意义的(人脑一眼就可以知道下一个还是不匹配,但朴素算法还是笨蛋的不断回溯)

可以看下面的例子:(人脑一眼就知道b不匹配a,希望下一步到a匹配a然后比较下一个是否匹配)

ababbba
abb
发现b与a不匹配
下一步:

朴素:
ababbba
 abb
我们希望:
ababbba
  abb

当然也不一定这么巧,下一步就直接是答案了。

知道了优化方案,接下来就是把理想转换为现实!!!

KMP思想:

朴素算法里主串的i是不断回溯的,但从上面的例子可以看出这种回溯是没有必要的,好马不吃回头草,KMP算法就是为了避免这种没有必要的回溯。

既然i不用回溯,那么我们就要考虑j的变化。通过上面的例子我们可以知道,P串手字符和后面字符比较,如果有相等的字符,j的变化就会不同,也就是说,j的变化和主串没有关系,关键取决于p串结构是否有重复,这种字符前后重复的长度成为前缀后缀和,只要求出最长前缀后缀和就知道j怎么变化了。 

这也引出了一个问题:如何求前缀后缀 ???

寻找最长前缀后缀(一大难点)

ABCABX
000120

模式串的各个字符前缀后缀最长前缀后缀
ANULLNULL0
ABAB0
ABCA,ABC,BC0
ABCAA,AB,ABCA,CA,BCA1
ABCABA,AB,ABC,ABCAB,AB,CAB,BCAB2
ABCABXA,AB,ABC,ABCA,ABCABX,BX,ABX,CABX,BCABX0

失配时,模式串向右移动的位数为:已匹配字符数 - 失匹配字符的上一位字符所对应的最长前缀后缀和。

我们发现,当匹配到一个字符失配时,其实没有必要考虑当前失配的字符,更何况我们每次失配时,都是看的失配字符的上一位字符对应的最大长度值。如此,便引入了next数组。

给定字符串“ABCDABD”,可求得它的next数组如下:

模式串ABCDABD
最大长度值0000120
next数组-1(约定)000012
s[i] != p[j]
i 不变
j = 0~j-1的最长前缀后缀
也就是:j = next[j]

规律总结:

发生失配时:

*如果next[j] != 0 则 j = next[j];

*否则下一次匹配j从0开始,意味着p串从头开始匹配

*次意味着什么呢?究其本质,next[j] = k 代表p[j] 之前的模式串子串中,有长度为 k 的相同前缀和后缀。有了这个 next 数组,在KMP匹配中,当匹配串中 j 处的字符失配时,下一步用next[j] 处的字符继续跟文本串匹配,相当于模式串向右移动j - next[j] 位。

这就是KMP的主干部分,剩下的难点就是如何求 next 数组了

KMP的算法流程

假设现在文本串 s 匹配到 i 位置,模式串p匹配到 j 位置;

*如果s[i] == p[j] ,令i++ , j++ , 继续匹配下一个字符;

*否则s[i] != p[j] ,令i不变, j = next[j];

*特殊情况:

若 j 回退到下标0的字符依旧失配,说明以当前的 i 开头的s串无法与 p 串配对,所以要将 i+1 ,从下一个开始尝试配对。为了统一和代码的简便,通常设 next[0] = -1,同时更改判断条件,完整的KMP流程如下:

流程:
初始化:i = 0,j = 0

循环条件:i < slen && j < plen

循环内部:
如果 j == -1 || s[i] == p[j] ,则 i++ , j++
否则 j = next[j]

循环外部:
j == m 说明找到子串
j != m 说明匹配失败

KMP的时间复杂度:o(n+m)

获得next数组的代码:

void getnext()
{
	int n = strlen(p);
	Next[0] = -1;
	int k = -1,j = 0;
	while(j<n)
	{
		if(k==-1||p[j]==p[k])
		Next[++j] = ++k;
		//next[j+1] = k+1;
		else
		k = Next[k];
		//k小于j,所以一直是回退 
	}
}

KMP代码:

int kmp()
{
	int n = strlen(s),m = strlen(p);
	int i = 0,j = 0;
	while(i<n&&j<m)
	{
		if(j==-1||s[i]==p[j])
		{
			i++;
			j++;
		}
		else
		j = Next[j];
	}
	if(j==m) return i-j;//下表从0开始 
	return -1;
}

完整代码:

#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;
const int N = 1e6+9;
char s[N],p[N]; 
int Next[N],ans[N];
void getnext()
{
	int n = strlen(p);
	Next[0] = -1;
	int k = -1,j = 0;
	while(j<n)
	{
		if(k==-1||p[j]==p[k])
		Next[++j] = ++k;
		//next[j+1] = k+1;
		else
		k = Next[k];
		//k小于j,所以一直是回退 
	}
}
int kmp()
{
	int n = strlen(s),m = strlen(p);
	int i = 0,j = 0;
	while(i<n&&j<m)
	{
		if(j==-1||s[i]==p[j])
		{
			i++;
			j++;
		}
		else
		j = Next[j];
	}
	if(j==m) return i-j;//下表从0开始 
	return -1;
}
int main()
{
	int round = 0;
	int n;
	while(~scanf("%d",&n)&&n)
	{
		scanf("%s",p);
		round++;
		getnext();
		printf("Test case #%d\n",round);
		for(int i = 1;i<=n;i++)
		{
			if(Next[i]&&i%(i-Next[i])==0)
			printf("%d %d\n",i,i/(i-Next[i]));
		}
		printf("\n");
	 } 
} 

KMP应用——最小循环节

kmp应用:

定理:假设 s 的长度为 len ,这 s 存在最小循环节,循环节的长度 L 为 len - next[len],子串为 s[0...len-next[len]-1]。

  1. 如果 len 可以被 len - next[len] 整除,则表明字符串 s 可以完全由循环节循环而成,循环周期 T = len/L。
  2. 如果不能,说明还需要再添加几个字母才能补全。需要补的个数是循环个数                 L - len%L        L = len - next[len]。

理解:

对于一个字符串,如abcd abcd abcd ,由长度为4的字符串abcd重复3次得到,那么必然有原字符串的前八位等于后八位。

也就是说,对于某个字符串 s ,长度为len,由长度为 L 的字符串 s 重复 R 次得到,当R>=2 时必然有 s[0...len-L-1] = s[L...len-1],字符串下标从0开始

那么对于KMP算法来说,就有next[len] = len-L。此时L肯定已经是最小的了(因为next的值是前缀和后缀相等的最大长度,即 len-L 是最大的,那么在 len 已经确定的情况下,L是最小的)。

例题:Power Strings

Given two strings a and b we define a*b to be their concatenation. For example, if a = "abc" and b = "def" then a*b = "abcdef". If we think of concatenation as multiplication, exponentiation by a non-negative integer is defined in the normal way: a^0 = "" (the empty string) and a^(n+1) = a*(a^n).

Input

Each test case is a line of input representing s, a string of printable characters. The length of s will be at least 1 and will not exceed 1 million characters. A line containing a period follows the last test case.

Output

For each s you should print the largest n such that s = a^n for some string a.

Sample Input

abcd
aaaa
ababab
.

Sample Output

1
4
3

Hint

This problem has huge input, use scanf instead of cin to avoid time limit exceed.

AC代码如下:

#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;
const int N = 1e6+9;
char s[N],p[N]; 
int Next[N];
void getnext()
{
	int n = strlen(p);
	Next[0] = -1;
	int k = -1,j = 0;
	while(j<n)
	{
		if(k==-1||p[j]==p[k])
		Next[++j] = ++k;
		//next[j+1] = k+1;
		else
		k = Next[k];
		//k小于j,所以一直是回退 
	}
}

int main()
{
	while(scanf("%s",p),p[0]!='.')
	{
		getnext();
		//最小循环节 
		int len = strlen(p);
		int L = len - Next[len];
		if(len%L==0)
		printf("%d\n",len/L);
		else
		printf("1\n");
	}
}

最后,感谢您的阅读!!!

ps:文章内容部分来自KMP算法详解,看完必会!_哔哩哔哩_bilibili视频

若有侵权请联系我,我将积极配合删除文章

这个视频对我有很大的帮助,同时我也非常推荐初学KMP算法的小伙伴们观看!!!

只要还有一口气,就继续战斗,继续呼吸,一直呼吸。——《荒野猎人》 

  • 4
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Joanh_Lan

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值