Skip to content

KMP 算法

2021-05-28

简介

KMP 是一种快速的字符串匹配算法,是其发明者 KruthMorrisPratt 的名字缩写。

问题

给定字符串 A(长度为 m)和 B(长度为 n),问 A 中是否包含 B

A=a b a b a b a b cB=a b a b c

考虑暴力算法:

  • 先将 AB 左端对齐;
  • 如果匹配失败,就将 B 右移一位,直到匹配成功。
A=a b a b a b a b cB=a b a b cA=a b a b a b a b cB=a b a b cA=a b a b a b a b cB=a b a b cA=a b a b a b a b cB=a b a b cA=a b a b a b a b cB=a b a b c

时间复杂度:O(mn)

cpp
bool match(string a, string b) {
    int m = a.size(), n = b.size();
    int i = 0, j = 0;
    while (i < m) {
        while (j < n && a[i] == b[j])
			j ++;
        if (j == n)
			return true;
        i ++, j = 0;
    }
    return false;
}

KMP 算法可以将此复杂度优化到 O(n)

原理

当匹配失败时,我们忽视了一些重要的信息。当第一次匹配失败时,两个绿串相等。

A=a b a b a b a b cB=a b a b c

该绿串还具有相同的前缀[1]和后缀,称作「公共前后缀」。abab 的公共前后缀为 ab

A=a b a b a b a b cB=a b a b c

B 右移后能与 A 匹配,那么 B 至少要右移 2 位,也就是使公共前后缀对齐。这样就避免了一位一位地移,从而提高效率。

A=a b a b a b a b cB=a b a b c

到此为止,KMP 的思路已经很明朗了。

A 串和 B 串左端对齐(σ 表示不定字符)。

A=σσσσσσσσσσσσB=σσσσσσσ

现在还有一个问题:在程序中,如何实现字符串的右移?—— 使用指针。

指针 i,j 分别指向 A 串和 B 串,表示 A[i]B[j] 之前 的两个绿串匹配。如果将 B 串右移 x 位,那么在程序中,只需要将 j 指针左移 x 位。

A=σσσσσσiσσσσσσB=σσσσσσjσA=σσσσσσiσσσσσσB=σσσjσσσσ

pre[i]B[0i] 的最长公共前后缀的长度。

现在假设 pre 数组已经处理好了。依照刚才的思路,KMP 的主程序可以分解为以下步骤:

  1. i,j 分别指向 AB 的开头,即 i=0,j=0
  2. 如果 A[i]=B[j]ij 同时右移一位;
  3. 如果 A[i]B[j],又分两种情况讨论:
    • j0 时,绿串的公共前后缀长度为 pre[j1]。那么就令 j=pre[j1]
    • j=0 时,j 不能再往左移。此时只能将 i 右移一位。
  4. 重复 2、3 两个步骤,直到匹配完成。

令指针 ij 分别指向 A 串和 B 串的开头。

A=aibababcB=ajbabc
cpp
int pre[];

bool kmp(string A, string B) {
	int i = 0, j = 0;
	while (i < A.size()) {
		if (A[i] == B[j])
			i ++, j ++;
		else if (j)
			j = pre[j - 1];
		else i ++;
		if (j == B.size()) //匹配成功
            return true;
	}
    return false;
}

预处理

pre 数组的求法和 KMP 的主程序很相似。

指针 i,j 都指向 B 串,表示当前求的是 B[0i] 的公共前后缀长度 pre[i]。并且 B[0i] 的公共前后缀为 B[0j],即 pre[i]=j+1

pre[0]=0 不用求,直接从 pre[1] 开始求就好,初始时 i=1,j=0

B=abaibcB=ajbabc

然后,我们直接采用类似 KMP 主程序 的策略:

  1. 如果 B[i]=B[j],那么 pre[i]=j+1,并将 ij 同时右移一位;
  2. 如果 B[i]B[j],又分两种情况讨论:
    • j0 时,绿串的公共前后缀长度为 pre[j1]。那么就令 j=pre[j1]
    • j=0 时,绿串长度为 0j 不能再往左移了.此时只能将 i 右移一位。
  3. 重复前两步,直到 pre[ ] 全部求完。

为什么可以这么做呢?因为仅当 B[i]B[j] 前的若干个字符相匹配时,B[0j] 才可能是公共前后缀。而 KMP 的主程序能够维护这一特征。

B=ababciB=abajbc

预处理 pre 数组,和 KMP 的主程序,其实是在干同一件事情。

cpp
int pre[];

void getPre(string B) {
	int i = 1, j = 0; // 注意 i = 1
	while (i < B.size()) {
		if (B[i] == B[j])
			pre[i] = j + 1, i ++, j ++;
		else if (j)
			j = pre[j - 1];
		else i ++;
	}
}

模板

cpp
const int N = 1e6;
int pre[N];

void getPre(string str) {
	int i = 1, j = 0;
	while (i < str.size()) {
		if (str[i] == str[j])
			pre[i] = j + 1, i ++, j ++;
		else if (j)
			j = pre[j - 1];
		else i ++;
	}
}

bool kmp(string str, string substr) {
	getPre(substr);
	int i = 0, j = 0;
	while (i < str.size()) {
		if (str[i] == substr[j])
			i ++, j ++;
		else if (j)
			j = pre[j - 1];
		else i ++;
		if (j == substr.size())
            return true;
	}
    return false;
}

int main() {
	string str, substr;
	cin >> str >> substr;
	cout << kmp(str, substr);
	return 0;
}

  1. 字符串 s 左部的任意子串为 s 的前缀,且 s 的前缀不能是 s 本身。例如 freeze 的前缀有 ffrfrefreefreez↩︎