从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[i-1]\)的时候就被枚举过了,所以我们要对这一部分操作进行精简。
    我们引入一个数组\(nxt[i]\),表示在模式串\(0\sim i-1\)的区间中,前缀和后缀相等的最大长度。
    假设以文本串的\(j\)位置为模式串的起始位置进行匹配,匹配到文本串的\(i\)位置(模式串的\(i-k\)位置)出现了失配的情况,按暴力算法我们会以文本串\(j+1\)的位置为模式串的起始位置继续匹配模式串。
    然而,我们发现,当出现失配的时候,我们已经有\(k\sim i-1\)区间内的文本串等于\(0\sim i-j-1\)区间内的模式串。
    \(nxt\)数组的定义易得在模式串中,有\(0\sim nxt[i-j]-1\)区间与\(i-k-nxt[i-j]\sim i-j-1\)区间相等
    所以模式串的\(0\sim nxt[i-j]-1\)也等于文本串的\(i-nxt[i-j]\sim i-1\)区间。
    所以这时我们可以视为我们以文本串的\(i-nxt[i-j]\)为起始位置进行匹配,正在匹配文本串中\(i\)位置的字符是否与模式串中\(nxt[i-j]\)位置的字符相等。
    所以,综上所述,我们在匹配字符串时,假设我们在匹配模式串中\(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(n^2)\),我们需要进行优化,
posted @ 2023-02-14 16:04  Kazdale  阅读(30)  评论(0编辑  收藏  举报