浅析字符串匹配算法——KMP算法(附CF 535D-Tavas and Malekas)

引入

在计算机中,人类的语言、文字等往往利用字符串进行保存,而字符串的匹配也对人们的生活产生着举足轻重的作用,例如个人信息的匹配、搜索引擎的使用等,为了在庞大的数据中找到我们所需的数据,字符串匹配的效率就决定了解决问题的难易程度。

实现

对于字符串的匹配,最简单且最容易实现的方法就是将模式串与文本串一一比对,观察每一位上的字符是否相等,如果不等则将模式串后移一位,再逐一进行比对,假定模式串的长度为 m m m,文本串的长度为 n n n,则需要进行 ( n − m ) × m \left ( n-m\right)×m (nm)×m次比较,而一般情况下 n > > m n>>m n>>m,所以朴素的枚举算法的时间复杂度为 O ( n × m ) O(n×m) O(n×m),在模式串和文本串足够大时,该算法的效率将变得低下。
在这里插入图片描述
为了解决这个问题,首先要观察是什么原因导致了算法的效率大打折扣。原因在于在每次比对过程中,一旦两个字符串某个位置不匹配,就要重新回溯,往后移动一个位置再从头进行比较。在有些情况下,这种回溯是没有意义的,例如对于文本串abababab和模式串ababc,一般的做法如下:

不难发现,其中第二次比对是没有任何意义的,为了避免这种不必要的回溯,首先要引入前缀和后缀的概念:

对于字符串 s 0 s 1 s 2 . . . s n − 1 s n s_0s_1s_2...s_{n-1}s_n s0s1s2...sn1sn,从 s 0 s_0 s0开始的任意子串(即不包含 s n s_n sn)称为该字符串的前缀,类比,以 s n s_n sn结尾的任意子串(即不包含 s 0 s_0 s0)称为该字符串的后缀,通过对模式串进行一个预处理,从而避免不必要的回溯,最终使得算法的复杂度降低为 O ( m + n ) O(m+n) O(m+n)

而KMP算法的核心步骤在于求解 nxt 数组,nxt 数组用于存储模式串中的前缀和后缀相同时的最大长度,其中 i 表示对于模式串其前缀 s 0 s 1 . . . s i − 1 s_0s_1...s_{i-1} s0s1...si1的长度,nxt[i] 表示该前缀所表示的字符串中前缀和后缀的最大公共长度。
实现代码如下:

void kmp()		//s是从1开始的,便于理解
{
	nxt[1] = 0;
	for (int i = 2, k = 0; i <= len; i++)
	{
		while (k && s[k + 1] != s[i])
		{
			k = nxt[k];
		}
		if (s[k + 1] == s[i])
		{
			k++;
		}
		nxt[i] = k;
	}
}

这段代码用最难以理解的地方就是循环中的while循环,举个例子,当 i = 20 时,模式串的前缀为 ababab…ababab (省略号中不重复,长度为20),此时代码执行完该循环后 k = 6,nxt[20] = 6。进入下一次循环,此时要比对 s[k + 1] 和 s[i],此时 k + 1 的值为 7 ,即比对上一次循环结束后的最长相同前缀的下一位与新加入的字符。

当相同时循环是不执行的,所以只是在上一次循环的基础上最大长度加 1 了而已,也不难理解。重点在于当 s[k + 1] != s[i] 时,循环体到底时如何进行重新匹配前缀和后缀的,考虑这样一个情况,由于当前最后一个字符不等,所以在考虑长度时前缀和后缀的范围应如下图所示:

那么如何来确定这个序列的长度呢,更一般的,假设第 i 次循环所确定的长度为 l l l,那么一定有 s 1 s 2 . . . s l = = s i − l + 1 . . . s i − 1 s i s_1s_2...s_l == s_{i-l+1}...s_{i-1}s_i s1s2...sl==sil+1...si1si,那么就有 s 1 s 2 . . . s l − 1 = = s i − l + 1 . . . s i − 2 s i − 1 s_1s_2...s_{l-1} == s_{i-l+1}...s_{i-2}s_{i-1} s1s2...sl1==sil+1...si2si1,根据这个等式可以发现,这就是前缀与后缀相等的情况,如图所示:

可以发现,新的前缀是原前缀的前缀,新的后缀是原后缀的后缀,而原前缀和原后缀是相等的,那么问题就转化为原前缀的前缀和后缀的最大长度是多少,而这个长度之前已存储在 nxt[l - 1] 中,也就是 nxt[k] 中,进行循环找到最大长度,如果不存在,那么会一直循环下去直到 k = nxt[1],即 k = 0,循环终止,对应也就是首字符和尾字符是否相等,如果不等,那这段序列也就没有相同的前缀和后缀。

例题 CF 535D-Tavas and Malekas



题目大意是给定一个字符串,然后给出    m    \;m\; m个位置,将该字符串插入,问能否可以形成一个合法的长度为    n    \;n\; n的字符串。分析题目不难得出思路:对于每次插入,只需要判断插入时是否会叠加,如果叠加了重合部分二者是否相等,即对应 KMP 算法的前缀与后缀问题。统计插入结束后还有多少个空位,答案就是 2 6 c n t 26^{cnt} 26cnt,(注意的点都写在注释里了)。
Solution:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll mod = 1e9 + 7;
const int maxn = 1e6 + 5;
inline ll read()
{
	ll t = 0;
	char ch = getchar();
	while (ch < '0' || ch>'9')
	{
		ch = getchar();
	}
	while (ch >= '0' && ch <= '9')
	{
		t = (t << 3) + (t << 1) + (ch ^ 48);
		ch = getchar();
	}
	return t;
}
inline void write(ll t)
{
	if (t > 9)
		write(t / 10);
	putchar(t % 10 + 48);
}
ll n, m, y, num, ans;
ll len, nxt[maxn];
char p[maxn];
inline void kmp()
{
	nxt[1] = 0;
	for (int i = 2, j = 0; i <= len; i++)
	{
		while (j && p[j + 1] != p[i])
		{
			j = nxt[j];
		}
		if (p[j + 1] == p[i])
		{
			j++;
		}
		nxt[i] = j;
	}
}
inline void quickPower()
{
	ans = 1;
	ll t = 26;
	while (num)
	{
		if (num & 1)
		{
			ans = (ans * t) % mod;
		}
		t = (t * t) % mod;
		num >>= 1;
	}
	ans %= mod;
}
int main()
{
	n = read();
	m = read();
	cin >> p + 1;
	len = strlen(p + 1);
	kmp();
	if (!m)			//如果没有一个插入位置,那么所有n个位置都有26种可能
	{
		num = n;
	}
	else
	{
		ll endPos = 0;		//记录每次插入结束后占据的最后一个位置
		while (m--)
		{
			y = read();
			if (endPos)
			{
				if (y > endPos)			//新插入位置与上一次插入不重合
				{
					num += y - endPos - 1;		//增加二者之间的空格
					endPos = y + len - 1;		//更新末尾位置
				}
				else
				{
					ll l = endPos - y + 1;
					ll h = nxt[len];
					bool flag = false;
					while (h)		//坑点,插入位置可能重合但不是最大长度,所以要循环判断每次可能
					{
						if (h < l)		//重合部分比最大长度还长,一定不符合要求
							break;
						if (h == l)		//二者重合合法,可以插入
						{
							flag = true;
							break;
						}
						h = nxt[h];
					}
					if (flag)		//重合位置符合要求,更新末尾位置
					{
						endPos = y + len - 1;
					}
					else
					{
						puts("0");
						return 0;
					}
				}
			}
			else
			{
				//对于第一次插入,无需判断重合,但是插入位置不一定是1号位,所以前面有y - 1个空位
				num = y - 1;
				endPos = y + len - 1;
			}
		}
		if (endPos > n)			//当最后一次插入时不足以全部插入,为不符合要求的情况
		{
			puts("0");
			return 0;
		}
		if (endPos < n)			//最后一次插入后末尾位置之后还有空位,需要补上
		{
			num += n - endPos;
		}
	}
	quickPower();
	write(ans);
	putchar(10);
	return 0;
}

完结撒花!!!

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值