串的模式匹配(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|\)。
转移函数为:
算法复杂度
时间复杂度 \(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\)。即,
若用 \(nxt_i\) 表示 \(i - move_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}\) 的一个相等前后缀。
枚举所有满足 \(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
算法优化
暂无
浙公网安备 33010602011771号