串的模式匹配(BF 算法,KMP 算法等)

前言

接下来,我们用 \(a_{l,r}\) 表示字符串 \(a\) 在下标 \(l\)\(r\) 内的所有字符组成的字串,用 \(|a|\) 表示字符串 \(a\) 的大小。

例如

\(a=^"abc^"\)

\(a_{1,2}=^"bc^"\)
\(|a|=3\)

串的模式匹配

有两个字符串 \(s\)\(t\),称 \(s\) 为目标串,\(t\) 为模式串。字符串模式匹配则是在 \(s\) 中寻找完全等于 \(t\) 的字串。

比如:

\(s:abcabcab\)
\(t:abcab\)

则可以匹配到 \(s_{3,7} = t\),没有其他匹配了。

BF 算法

全称 Brute-Force 算法。

算法复杂度

时间复杂度 \(O((|s|-|t|+1)|t|)\),即 \(O(|s||t|)\)

空间复杂度 \(O(|s|+|t|)\)

算法思想

顾名思义,他是一种暴力算法。

即挨个判断 \(t\) 在每个位置下 \(s\) 对应子串下是否与 \(t\) 匹配。

算法流程

枚举起始对齐位置 \(i\),其中 \(0 \leq i \leq |s| - |t|\)

对于每个位置 i,暴力匹配 \(s_{i,i+|t|-1}\)\(t\) 的每一位,判断是否相等。

相等,匹配成功。

算法伪代码

find():
n = size s
m = size t
for i in [0, n - m]:
	flag = true
	for j in [0, m - 1]:
		if s[i + j] != t[j]:
			flag = false
	if flag = true:
		match
	else:
		dismatch
end

算法优化

当发现 \(s_{i, i+|t|-1}\)\(t\) 有一字符不匹配时,直接终止当前相等判断。

话说这算优化吗

KMP 算法

KMP 算法由 D.E.Knuth、J.H.Morris、和 V.R.Pratt 提出。在某种意义上来说是 BF 算法的改进算法,将字符串模式匹配算法的速度大幅提升。

KMP 算法可以被视为一种自动机,KMP 自动机,接受且仅接受以 \(s\) 为后缀的字符串,接受状态为 \(|s|\)

转移函数为:

\[\delta(i,c)=\begin{cases}i+1&s_{i+1}=c\\ 0&s_1\not=c \operatorname{and} i = 0\\ \delta(\pi(i),c) & s_{i+1}\not=c \operatorname{and} i > 0 \end{cases}\]

算法复杂度

时间复杂度 \(O(|s|+|t|)\)

空间复杂度 \(O(|s|+2|t|)\)

算法思想

原来 BF 算法进行了许多没有必要的匹配。

比如说:

\(a = ^"abcabcd^"\)
\(b = ^"abcd^"\)

当进行第一次匹配,即匹配 \(s_{0,3}\)\(t\) 时,发现 \(s_3\)\(t_3\) 不匹配。

此时,BF 算法会将匹配位置右移一位,进行 \(s_{1,4}\)\(t\) 的匹配。

但是显然,这不可能成功,因为刚才匹配结果表示 \(s_1 = t_1\),而 \(t_1 \not= t_0\),所以 \(s_1 \not= t_0\)

根据刚才匹配结果,\(s_0 = t_0, s_1=t_1, s_2=t_2, s_3\not=t_3\),因为我们知道 \(t\) 字符间互不相等,所以最好将匹配位置右移 \(3\) 位,省去不必要的计算。

根据这个情况,我们可以尝试对模式串 \(t\) 进行预处理。来计算每个失去匹配的位置 \(i\) 需要匹配位置右移的位数 \(move_i\)。(\(nxt\) 数组与此数组有关,或者此数组可以通过 \(nxt\) 推出)

那我们如何计算 \(move_i\)

此时,假设在第 \(j\)\(s_j\) 失去匹配。可以得到 \(\forall i\in[0,j], s_i = t_i\)。但是我们不能直接将匹配位置右移 \(j\) 位,因为这样可能错过一个匹配。

比如说:

\(a = ^"abacabacabc^"\)
\(b = ^"abacabc^"\)

假设要右移 \(k\) 位,我们则需要满足 \(\forall i\in[0, 3 - k] t_i=t_{i+k}\)。因为不能错过任何可能的匹配,所以还需要满足 \(\exists i\in[0, 3 - k + 1] t_i\not=t_{i+k-1}\)

也就是,假设要右移 \(k\) 位,则需要满足 \(t_{0, j-k} = t_{k, j}\)\(t_{0, j-k+1} \not= t_{k - 1, j}\)

最终右移的位数 \(move_i\) 就是满足 \(t_{0, i-k} = t_{k, i}\) 的最小的 \(k\)。即,

\[move_i=\min\limits_{0\leq k\leq i} \{k|t_{0, i-k} = t_{k, i}\} \]

若用 \(nxt_i\) 表示 \(i - move_i\),则,

\[nxt_i=\max\limits_{0\leq k\leq i} \{k|t_{0, k} = t_{i-k, i}\} \]

为什么要用 \(nxt_i\) 表示?一是比较好计算,二是 \(nxt_i + 1\) 其实是 \(t_{0,i}\) 的最长相等前后缀长度。(或许是我瞎扯的一个名字)

\(nxt_i \not= i\),因为不能不移动。

预处理出 \(nxt\) 数组即可。

此处我们尝试用递推预处理,从 \(nxt_{i-1}\) 推出 \(nxt_i\)

\(t_{0, nxt_{i}} = t_{i-nxt_{i}, i}\),所以 \(t_{0, nxt_{i}-1} = t_{i-nxt_{i}, i-1}\)

\(t_{0, nxt_{i}-1}\) 一定是 \(t_{0,i-1}\) 的一个相等前后缀。

\[nxt_{i} = \max_{t_{0,j}=t_{i-j-1,i-1}} \{j+1|t_{j+1}=t_{i}\} \]

枚举所有满足 \(t_{0,j}=t_{i-j-1,i-1}\)\(j\) 即可。

显然无法直接枚举所有 \(j\),判断是否满足 \(t_{0,j}=t_{i-j-1,i-1}\),我们需要用其他方法。

假设 \(t_{0,j}=t_{i-j-1,i-1}\),若下一个相等前后缀长度 \(k < j\) 也满足 \(t_{0,k}=t_{i-k-1,i-1}\)

\(t_{0,k} = t_{i-k-1,i-1} = t_{j - k, j}\),所以说 \(k\)\(t_{0,j}\) 的相等前后缀。

为了不漏掉任何一个相等前后缀,下一个枚举的 \(k\) 需要为 \(j\) 的最长相等前后缀即 \(nxt_j\)

所以从 \(j = nxt_{i-1}\) 开始枚举,每次 \(j = nxt_j\),就可以遍历所有相等前后缀。

算法流程

参见伪代码

算法伪代码

init():
m = size t
j = -1
nxt[0] = -1
for i in [1, m - 1]:
	for t[j + 1] != t[i] and j != -1:
		j = nxt[j]
	if t[j + 1] = t[i]:
		j = j + 1
	nxt[i] = j
end

find():
n = size s
m = size t
for i in [0, n]
	for s[i + j + 1] == t[j + 1] and j + 1 <= m
		j = j + 1
	if j = m
		match
	i = i + j - nxt[j]
	j = nxt[j]
end

算法优化

暂无

posted on 2023-02-13 12:47  Evan_song  阅读(60)  评论(0)    收藏  举报