日常算法学习--字符串

KMP

原理及用法

KMP主要用于解决字符串匹配类问题。典型的例题是给定一个长度为N的字符串 S S S和一个长度为M的模式串 P P P,求 P P P S S S中出现了几次。
若我们朴素的枚举 S S S的每个位置来匹配 P P P的话,时间复杂度是 O ( N M ) O(NM) O(NM)的,这显然是不能接受的。
所以我们可以使用KMP算法来解决此题,它可以在 O ( N + M ) O(N+M) O(N+M)内得出答案。

朴素的做法中我们每次匹配失败时,会将 P P P从头开始匹配,这也是浪费大量时间的主要原因。而KMP算法则使用预先记录的 n e x t next next数组保证了匹配失败时我们回退 P P P到与 S S S的最长后缀相匹配的最长前缀处的位置,这保证了我们遍历 S S S P P P串最多回退 N + M N+M N+M次。
n e x t next next存储的是模式串 P P P每个位置最近的回退位置,实际上 n e x t next next数组的求法与S串无关,因为与 S S S最长后缀相匹配时其实就是与 P P P的最长后缀相匹配。

inline void kmp() {
	n = strlen(s + 1);
	m = strlen(p + 1);

	for (int i = 2, j = 0; i <= m; i ++ ) {
		while (j && p[i] != p[j + 1]) j = nt[j];
		if (p[i] == p[j + 1]) j ++;
		nt[i] = j;
	}
	for (int i = 1, j = 0; i <= n; i ++ ) {
		while (j && s[i] != p[j + 1]) j = nt[j];
		if (s[i] == p[j + 1]) j ++;
		f[i] = j;
	}
}

我们可以发现实际上求 n e x t next next数组的过程和求答案数组 f f f的过程是一样的,这无疑增加了代码的冗余度
所以我们可以将 P P P S S S首尾相接,使用后半段的 n e x t next next数组来代替 f f f 数组,注意相接时在中间添加一个特殊字符,防止后段的后缀计算同时计算了原来 P P P串的部分

inline void kmp() {
	n = strlen(s + 1);
	m = strlen(p + 1);
	p[m + 1] = '@';
	for (int i = 1, j = m + 2; i <= n; i ++, j ++ ) p[j] = s[i];

	for (int i = 2, j = 0; i <= n + m + 1; i ++ ) {
		while (j && p[i] != p[j + 1]) j = nt[j];
		if (p[i] == p[j + 1]) j ++;
		nt[i] = j;
	}
}
题目
字符串匹配例题

在这里插入图片描述

思路

KMP模板题

代码
inline void kmp() {
	n = strlen(s + 1);
	m = strlen(p + 1);
	p[m + 1] = '#';
	for (int i = 1, j = m + 2; i <= n; i ++, j ++ ) p[j] = s[i];

	int len = n + m;
	int j = 0;
	nt[1] = 0;

	for (int i = 2; i <= n + m + 1; i ++ ) {
		while (j && p[i] != p[j + 1]) j = nt[j];
		if (p[i] == p[j + 1]) j ++;
		nt[i] = j;
	}

	int cnt = 0;
	for (int i = m + 2; i <= n + m + 1; i ++ ) 
		cnt += (nt[i] == m);
	if (!cnt) puts("-1\n-1");
	else {
		printf("%d\n", cnt);
		for (int i = m + 2; i <= n + m + 1; i ++ ) 
			if (nt[i] == m) printf("%d ", i - 2 * m);
		printf("\n");
	}

}
Secret word

在这里插入图片描述

思路

一个结论: 字符串的最小循环节等于 n − n e x t [ n ] n - next[n] nnext[n]

代码
inline void kmp() {
	m = strlen(p + 1);
	nt[1] = 0;

	for (int i = 2, j = 0; i <= m; i ++ ) {
		while (j && p[i] != p[j + 1]) j = nt[j];
		if (p[i] == p[j + 1]) j ++;
		nt[i] = j;
	}

	printf("%d\n", m - nt[m]);
}
Secret word

在这里插入图片描述

思路

本题我们可以将s反转后相接在s后,next记录的后缀值就能对应当前的前缀值

代码
inline void kmp() {
	m = strlen(p + 1);
	nt[1] = 0;
	p[m + 1] = '@';
	for (int i = m, j = m + 2; i >= 1; i --, j ++) p[j] = p[i];

	for (int i = 2, j = 0; i <= 2 * m + 1; i ++ ) {
		while (j && p[i] != p[j + 1]) j = nt[j];
		if (p[i] == p[j + 1]) j ++;
		nt[i] = j;
	}
	int mx = 0;
	for (int i = m + 2; i <= 2 * m + 1; i ++ ) mx = max(mx, nt[i]);
	for (int i = mx; i >= 1; i -- ) printf("%c", p[i]);
	puts("");
}

EXKMP(扩展KMP)

原理及用法

K M P KMP KMP主要计算字符串某一位置的向前的最大后缀相匹配的最大前缀的功能相反,扩展 K M P KMP KMP则为了解决字符串某一位置到字符串尾的最大后缀与最大前缀相匹配的问题。
而且扩展 K M P KMP KMP的思路相比 K M P KMP KMP来说也更好理解,我们只要从前往后维护一个 [ L , R ] [L,R] [L,R]的区间,区间内部的点可以直接通过目前后缀所对应的前缀位置已经得到的答案来初步计算,如果已得出的后缀长度没有超出所维护的区间的长度,则前后缀的答案是相等的,否则就向后暴力枚举更新位置。
而区间外的位置同样只需暴力枚举答案即可,因为整个字符串向后枚举的最多N次,所以扩展KMP的时间复杂度是 O ( N ) O(N) O(N)的。

int L = 1, R = 0;
for (int i = 2; i <= n; i ++ ) {
	if (i <= R) {
		int k = i - L + 1;
		z[i] = min(z[k], R - i + 1);
	}
	while (i + z[i] <= n + m + 1 && p[1 + z[i]] == p[i + z[i]]) ++ z[i];
	if (i + z[i] - 1> R) L = i, R = i + z[i] - 1;
}

其中 z [ ] z[] z[]数组的实际意义是与KMP n e x t next next数组相对应的

题目
字符串匹配例题
思路

本题同样可以使用 EXKMP来得出答案,注意前提是必须将两个字符串合并来计算 z [ ] z[] z[]数组

代码
inline void exkmp() {
	n = strlen(s + 1);
	m = strlen(p + 1);

	p[m + 1] = '@';
	for (int i = m + 2, j = 1; j <= n; i ++, j ++ ) p[i] = s[j];

	int L = 1, R = 0;
	for (int i = 2; i <= n + m + 1; i ++ ) {
		if (i <= R) {
			int k = i - L + 1;
			z[i] = min(z[k], R - i + 1);
		}
		while (i + z[i] <= n + m + 1 && p[1 + z[i]] == p[i + z[i]]) ++ z[i];
		if (i + z[i] - 1> R) L = i, R = i + z[i] - 1;
	}
	int cnt = 0;
	for (int i = m + 2; i <= n + m + 1; i ++ ) cnt += (z[i] == m);
	if (!cnt) puts("-1\n-1");
	else {
		printf("%d\n", cnt);
		for (int i = m + 2; i <= n + m + 1; i ++ ) 
			if (z[i] == m) printf("%d ", i - m - 1);
		puts("");
	}
}
Password

在这里插入图片描述

思路

根据 z [ ] z[] z[]数组的实际意义,当 z [ i ] z[i] z[i] n − i + 1 n - i + 1 ni+1时,则这个位置的后缀刚好是字符串的后缀及前缀,其满足了题目中的前后缀要求,而剩余的中间位置出现的条件我们可以转化为从当前 i i i位置向前能否找到一个位置的 z [ j ] z[j] z[j] 大于 i − n + 1 i-n+1 in+1,因为匹配了更长的前缀显然可以匹配更短的前缀,我们可以截取长度为 i − n + 1 i-n+1 in+1的前缀来匹配i位置的后缀。
所以我们只要从前往后遍历字符串找一个最大的后缀长度同时满足以上两个条件即可。

代码
inline void exkmp() {
	m = strlen(p + 1);

	int L = 1, R = 0;
	for (int i = 2; i <= m; i ++ ) {
		if (i <= R) {
			int k = i - L + 1;
			z[i] = min(z[k], R - i + 1);
		}
		while (i + z[i] <= m && p[1 + z[i]] == p[i + z[i]]) ++ z[i];
		if (i + z[i] - 1> R) L = i, R = i + z[i] - 1;
	}
	int ans = 0, x = 0;
	// for (int i = 1; i <= m; i ++ ) cout << i + z[i] - 1 << ' ';
	// 	cout << endl;
	for (int i = 1; i <= m; i ++ ) {
		if (i + z[i] - 1 == m)
			if (x >= z[i]) {
				ans = z[i];
				break;
			}

		x = max(x, z[i]);
	}
	if (!ans) puts("Just a legend\n");
	else {
		for (int i = 1; i <= ans; i ++ ) printf("%c", p[i]);
		puts("");
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Pale_B

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

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

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

打赏作者

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

抵扣说明:

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

余额充值