从KMP到ExKMP

  • KMP(Lead-in)

    KMP算法全称Knuth-Morris-Pratt算法,可以在O(n+m)的时间复杂度下进行在长度为n的字符串(文本串)中查找另一个长度为m的字符串(模式串)出现的所有位置,同时也能在O(n)的时间复杂度下查找一个长度为n的字符串中,前缀和后缀相同的最大长度。
    首先思考一下,如何暴力地在一个字符串中查找另一字符串出现的所有位置,很快可以找到这样一种思路:枚举文本串中所有位置作为模式串的首个字符,然后向后遍历判断这个字符之后是否存在模式串,实现代码如下:
	#include <iostream>
	using namespace std;
	int main() {
		ios::sync_with_stdio(false);
		string a, b;
		int ans = 0;
		cin >> a >> b;
		int la = a.length(), lb = b.length();
		for (int i = 0; i < la; ++i) {
			if (a[i] == b[0]) {
				for (int j = 0; j < lb; ++j) {
					if (a[i + j] != b[j])  break;
					if (j == lb - 1) ++ans;
				}
			}
		}
		cout << ans << endl;
		return 0;
	}
  • KMP(查找)

    显然,此时程序的复杂度是O(nm)的,我们需要对时间复杂度进行优化。
    我们可以发现,在上一个代码中,当我们枚举一个字符s[i]时,这个字符很可能在我们枚举s[i1]的时候就被枚举过了,所以我们要对这一部分操作进行精简。
    我们引入一个数组nxt[i],表示在模式串0i1的区间中,前缀和后缀相等的最大长度。
    假设以文本串的j位置为模式串的起始位置进行匹配,匹配到文本串的i位置(模式串的ik位置)出现了失配的情况,按暴力算法我们会以文本串j+1的位置为模式串的起始位置继续匹配模式串。
    然而,我们发现,当出现失配的时候,我们已经有ki1区间内的文本串等于0ij1区间内的模式串。
    nxt数组的定义易得在模式串中,有0nxt[ij]1区间与iknxt[ij]ij1区间相等
    所以模式串的0nxt[ij]1也等于文本串的inxt[ij]i1区间。
    所以这时我们可以视为我们以文本串的inxt[ij]为起始位置进行匹配,正在匹配文本串中i位置的字符是否与模式串中nxt[ij]位置的字符相等。
    所以,综上所述,我们在匹配字符串时,假设我们在匹配模式串中j位置上的字符时出现失配的情况,我们只需让j=nxt[j]然后继续与文本串内的字符继续向后比对,如果再次失配就再次跳nxt,当j指向模式串最后一位的下一位时就找到了文本串中的一个模式串。
    易得代码实现如下:
	for (int j = 0, i = 0; i < len1; ++i) {
		while (j && (s1[i] != s2[j]))  j = nxt[j];
		if (s1[i] == s2[j])  ++j;
		if (j == len2)  cout << i - len2 + 1 << endl;
	}
  • KMP(求nxt)

    接下来我们就要求nxt数组,假设我们已经求出来了nxt[j],然后我接下来要求nxt[j+1],那么我们只需要确认s[nxt[j]+1]s[j+1]是否相等,如果不相等就跳到下一个nxt,发现基本的思想就是自己跟自己匹配,那么易得代码如下所示(i和j要错开要不然一直是相同的)
	for (int j = 0, i = 1; i < len2; ++i) {
		while (j && (s2[i] != s2[j]))  j = nxt[j];
		if (s2[i] == s2[j])  nxt[i + 1] = ++j;
		else  nxt[i + 1] = 0;
	}
  • KMP(时间复杂度)

    我们可以发现KMP算法只需要对模式串和文本串各遍历一遍即可(少数的跳nxt操作基本上可以忽略)。
    我们优化的思路是尽可能地运用我们已经得出的结果,即尽量从已经得出的状态转移到一个新状态。
  • Manacher(Lead-in)

    Manacher算法可以在O(n)的时间复杂度下解决寻找一个字符串内最长回文字符串的问题,这个问题用暴力很好解决。
    枚举回文串的奇偶性,若是奇数则枚举中心字符向左右两边扩展,若是偶数就枚举最中心偏左的字符向左右两边扩展。
    这个算法分奇偶性,显然并不美观,那我们可以将偶数时也枚举中心,但是枚举的是两个字符间的空格。
    然后我们可以将原字符串的空格都用'#'字符代替,然后字符串的左右两头用不同的字符代替。
    举个例子,若原字符串是"Kazdale",则转换后的字符串则为“#K#a#z#d#a#l#e#.”,我们设若枚举i为中心,则中心加上回文串的右半边的长度为pdr[i],例如字符串aba(转换后为#a#b#a#.),它的pdr[4]就等于4,易证字符串转换后求出的pdr[i]1即为以i为中心时的回文串长度。
  • Manacher

    暴力算法的时间复杂度为O(n2),我们需要进行优化,
posted @   Kazdale  阅读(33)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示