KMP&拓展KMP 复习笔记

KMP

引入

思考一个经典问题:求模式串 T T T在文本串 S S S中第一次出现的位置。


考虑朴素做法:枚举 S S S的每一位考虑将其设为 T T T开头所处的位置,依次判断,复杂度 O ( ∣ S ∣ × ∣ T ∣ ) O(|S|\times |T|) O(S×T)
换句话说,若匹配,比较下位;否则,回到上次的下一个位置位置继续匹配,即 S i ≠ T j S_i \neq T_j Si=Tj时, i → i − j + 1 , j → 0 i\rightarrow i-j+1,j\rightarrow 0 iij+1,j0
能否减少跳转,使 i i i线性, j j j尽量少跳转,达到 O ( n + m ) O(n+m) O(n+m)的时间复杂度?请看下文KMP算法。

算法

考虑失配情况。虽然当前为不匹配,但之前位是完全匹配的,即 S i − j . . . i − 1 = T 0... j − 1 S_{i-j...i-1}=T_{0...j-1} Sij...i1=T0...j1,这也正是我们所遗漏的条件。
令此时 j j j跳转到 k k k继续和 S i S_i Si匹配,即:
在这里插入图片描述

图中的两次等于列成式子就是:
{ S i − j . . . i − 1 = T 0... j − 1 S i − k . . . i − 1 = T 0... k − 1 \begin{cases} S_{i-j...i-1}=T_{0...j-1}\\ S_{i-k...i-1}=T_{0...k-1} \end{cases} {Sij...i1=T0...j1Sik...i1=T0...k1
这个式子是关于两个字符串的,有些复杂,可否将其化成仅关于 T T T的等式,即消掉等式左边的 S S S?由第一个等式进一步推得 S i − k . . . i − 1 = T j − k . . . j − 1 S_{i-k...i-1}=T_{j-k...j-1} Sik...i1=Tjk...j1,将第二个式子带入,得 T 0... k − 1 = T j − k . . . j − 1 T_{0...k-1}=T_{j-k...j-1} T0...k1=Tjk...j1

也就是 T T T的前缀和后缀相同,因为我们要尽量少往回跳,所以 k k k要尽量大;而且,这个前缀和后缀不能是其本身,所以 k < j k<j k<j此时符合条件的 k k k即为 f a i l j fail_j failj,没有即为-1

举个栗子,求字符串"qwqwqawawa"的 f a i l fail fail

字符fail
q-1
w-1
q0
w1
q2
a-1
w-1
a-1
w-1
a-1

如何求出 f a i l fail fail数组?不着急,先看如何利用求好的 f a i l fail fail匹配。

int KMP(char *s, char *t) {
	int n = strlen(s), m = strlen(t), j = -1;
	for (int i = 0; i < n; ++i) {
		while (~j && t[j + 1] != s[i]) {	// 失配:跳转
			j = fail[j];
		}
		if (t[j + 1] == s[i]) {	// 匹配:下一位
			++j;
			if (j + 1 == m) {	// 第一次匹配完所有字符:返回
				return i - m + 1;
			}
		}
	}
	return -1;
}

我们对着这个代码想,就可以得出,求解 f a i l fail fail的过程即是 T T T自身匹配的过程,用前面的 f a i l fail fail算出后面的 f a i l fail fail,代码类似。

void get_fail(char *s) {
	int n = strlen(s);
	fail[0] = -1;
	for (int i = 1; i < n; ++i) {
		int j = fail[i - 1];
		while (~j && s[j + 1] != s[i]) {
			j = fail[j];
		}
		if (s[j + 1] == s[i]) {
			++j;
		}
		fail[i] = j;
	}
}

习题

关于kmp的习题,不仅是关于其主要求解问题——字符串匹配,有部分利用了其 f a i l fail fail数组。

POJ 2406 Power Strings

这里就充分利用的 f a i l fail fail数组的特性, f a i l i fail_i faili为前 i i i个字符的最短公共前后缀的长度,既是最短,又是公共,不就是要求的循环节吗?这里我们需要判断 n n n是否可以被 f a i l n fail_n failn整除:若整除,答案为 n f a i l n \frac{n}{fail_n}% failnn;否则,答案为 1 1 1 s s s本身)。

POI2006 OKR-Periods of Words

这道题需要对 f a i l fail fail做二次处理,即长度减去重叠部分长度(长度-最小周期=最大周期),最后再将长度减去处理后 f a i l fail fail求和即可。

CF1137B Camp Schedule

我们可以从 s s s中得到 0 0 0 1 1 1的数量,则考虑填充 t t t。注意因为前缀后缀可能相同,所以不需要填充整个 t t t,而公共前后缀正好匹配 f a i l fail fail的性质,则每次填充完跳转到 f a i l fail fail处即可。

拓展KMP

这里应用没有kmp算法广泛,本文中适当略写。

问题

S S S所有后缀与 T T T的最长公共前缀(lcp),要求时间复杂度 O ( ∣ S ∣ + ∣ T ∣ ) O(|S|+|T|) O(S+T)

算法

思想和kmp有些类似,将其 f a i l fail fail数组改为 n e x t next next,含义由字符串前缀和此位置结束子串相等转变为和此位置开始子串相等,还是基于 T T T构造

next

还是递推。设 n e x t 0... x − 1 next_{0...x-1} next0...x1均已求得,求 n e x t x next_x nextx
定义 p p p为之前所有位置能匹配到的最远位置,即 m a x { i + n e x t i − 1 } max\{i + next_i - 1\} max{i+nexti1},令 k k k为此时的 i i i,则根据 n e x t next next的定义,红色部分相等:
在这里插入图片描述
从两部分各取长度 p − x + 1 p-x+1 px+1后缀,则绿色部分相等:
在这里插入图片描述
y = n e x t x − k y=next_{x-k} y=nextxk,则与红色部分相同,蓝色部分相等:

在这里插入图片描述
又因红色部分相等,三处蓝色部分相等:
在这里插入图片描述
所以,如果一个蓝色的长度比一个绿色的长度小,也就是说后面位置的lcp不能比前面的还大或者等于,形式化的, y < p − x + 1 → x + y ≤ p y<p-x+1 \rightarrow x+y\le p y<px+1x+yp那么 n e x t x = y next_x=y nextx=y;否则,两个指针分别从 y + 1 y+1 y+1 p + 1 p+1 p+1开始枚举。

void get_next(char *s) {
	int n = strlen(s), p = 0, k = 1;
	next[0] = n;
	while (p < n - 1 && s[p] == s[p + 1]) {
		++p;
	}
	next[1] = p;	// next[0 & 1]: 手动计算
	for (int i = 2; i < n; ++i) {
		p = k + next[k] - 1;
		int y = next[i - k];
		if (i + y <= p) {	// 分类讨论
			next[i] = y;
		} else {
			int j = max(p - i + 1, 0);	// 注意特判
			while (i + j < n && s[i + j] == s[j]) {
				++j;
			}
			next[i] = j, k = i;	// k直接更新,最新匹配一定比之前的p大
		}
	}
}

ex

刚才说的这么多,有童鞋会问,整个 n e x t next next都是基于 T T T后缀与 T T T匹配的,如何用来进行 S S S后缀与 T T T的匹配呢?
我们定义 e x i ex_i exi S S S的后缀 i i i T T T的lcp,还是设 e x 0... x − 1 ex_{0...x-1} ex0...x1均已求出,求 e x x ex_x exx
同理,已知 S k . . . p = T 1... e x k S_{k...p}=T_{1...ex_k} Sk...p=T1...exk p = m a x { i + n e x t i − 1 } p=max\{i + next_i - 1\} p=max{i+nexti1}
我们可以先利用 n e x t k next_k nextk求出红色部分,再利用 n e x t x − k next_{x-k} nextxk求出蓝色部分,其余过程一样。

void get_ex(char *s, char *t) {
	int n = strlen(t), m = strlen(s), p = 0, k = 0;
	while (p < min(n, m) && s[p] == t[p]) {
		p = 0;
	}
	ex[0] = p;
	for (int i = 1; i < m; ++i) {
		p = k + ex[k] - 1;
		int y = next[i - k];
		if (i + y <= p) {
			ex[i] = y;
		} else {
			int j = max(p - i + 1, 0);
			while (i + j < m && j < n && s[i + j] == t[j]) {
				++j;
			}
			ex[i] = j, k = i;
		}
	}
}

今天复习内容就到这里,感谢收看~

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

蒟蒻一枚

谢谢鸭~

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

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

打赏作者

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

抵扣说明:

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

余额充值