前言
关于字符串匹配,最常见使用最广泛的即 K M P KMP KMP算法,该算法通过对模式串的子串的前后缀进行处理而减少回溯次数,提高匹配效率,相关内容参考浅析字符串匹配算法——KMP算法。此文主要讨论字符串的另一种匹配算法—— Z \;Z\; Z算法/ Z − B O X \;Z-BOX\; Z−BOX算法。
原理
何为
Z
\;Z\;
Z算法,
Z
\;Z\;
Z算法的主要设计思想是通过一个
z
z
z数组来记录一个字符串的后缀与该字符串的最大前缀匹配长度。换句话说,
z
[
i
]
\;z\left [ i \right]\;
z[i]表示对于字符串下标为
i
\;i\;
i的字符开始,与字符串的最大前缀匹配长度。例如,对于字符串
a
a
a
b
a
a
b
a
\;aaabaaba\;
aaabaaba,对应的
z
\;z\;
z函数为:
那么如何求得每一个字符对应的
z
[
i
]
\;z\left [ i \right]\;
z[i]值呢。在进行计算过程中,我们需要两个关键的信息,即对于
s
[
i
]
\ s\left [ i \right]\;
s[i]而言,在计算
z
[
i
]
\;z\left [ i \right]\;
z[i]时,
z
[
i
]
\;z\left [ i \right]\;
z[i]的区间范围是多少,用
l
\;l\;
l和
r
\;r\;
r来记录,其定义为当前元素所被包含范围内
r
\;r\;
r最大的区间,也就是说对于每一个
z
[
i
]
\;z\left [ i \right]\;
z[i],同时称这个区间为一个盒子,也就是一个范围为
[
l
,
r
]
\left [ l,r \right]\;
[l,r]的
b
o
x
box
box所盖住的元素。我们可以根据前
i
−
1
\;i-1\;
i−1个
z
\;z\;
z函数以及不断维护的
l
\;l\;
l和
r
\;r\;
r来求取当前
z
[
i
]
\;z\left [ i \right]\;
z[i]的值。
这么说还是很抽象难以理解,拿上述例子来说,对于 s [ 0 ] \;s\left [ 0 \right]\; s[0]很显然其值为 s . l e n g t h \;s.length\; s.length,此时将 l \;l\; l和 r \;r\; r初始化为0,对于 s [ 1 ] \;s\left [ 1 \right]\; s[1]计算得到 s [ 12 ] = = s [ 01 ] \;s\left [ 12 \right]\;==\;s\left [ 01 \right]\; s[12]==s[01],所以我们将 z [ 1 ] \;z\left [ 1 \right]\; z[1]设置为2,同时更新 l = 1 , r = 2 \;l=1,\;r=2\; l=1,r=2;紧接着对于 s [ 2 ] \;s\left [ 2 \right]\; s[2],可以得到 s [ 2 ] = = s [ 0 ] \;s\left [ 2 \right]==\;s\left [ 0 \right]\; s[2]==s[0],将 z [ 2 ] \;z\left [ 2 \right]\; z[2]设置为1,不更新 l \;l\; l和 r \;r\; r,这是因为根据定义 l \;l\; l和 r \;r\; r是盖住当前元素的 b o x box box右边界最大的区间范围,在这次计算过程中 r \;r\; r仍为2,所以区间范围不变。同样方法可以求出所有元素的 z [ i ] \;z\left [ i \right]\; z[i]值 (理解 l \;l\; l和 r \;r\; r的含义对算法实现十分关键)。
那么设置的
l
\;l\;
l和
r
\;r\;
r对求
z
[
i
]
\;z\left [ i \right]\;
z[i]有什么作用呢,对于维护的
l
\;l\;
l和
r
\;r\;
r我们首先能得到的信息是:
s
0
s
1
.
.
.
s
r
−
l
=
=
s
l
s
l
+
1
.
.
.
s
r
s_0s_1...s_{r-l}==s_ls_{l+1}...s_r
s0s1...sr−l==slsl+1...sr。
考虑如下几种情况:
1.
此时我们要计算的
z
[
i
]
\;z\left [ i \right]\;
z[i]的取值已经超出了
b
o
x
\;box\;
box的范围,所以这时候已有的
b
o
x
\;box\;
box已经不能为我们提供有用信息,此时只能通过枚举逐一比对,如果存在与前缀相同的后缀,更新
l
=
i
,
r
=
l
e
n
g
t
h
\;l=i,r=length
l=i,r=length。
2.
对于
i
\;i\;
i位于
[
l
,
r
]
\left [ l,r \right]
[l,r]中此时
l
\;l\;
l和
r
\;r\;
r就起到了关键作用。
i
\;i\;
i位于
b
o
x
\;box\;
box那么一定就有
s
i
−
l
s
i
−
l
+
1
.
.
.
s
r
−
l
=
=
s
i
s
i
+
1
.
.
.
s
r
\;s_{i-l}s_{i-l+1}...s_{r-l}==s_{i}s_{i+1}...s_{r}\;
si−lsi−l+1...sr−l==sisi+1...sr,那么根据
z
[
i
−
l
]
\;z\left [ i-l \right]\;
z[i−l]的大小又可以分为如下两种情况:
如果
z
[
i
−
l
]
<
r
−
i
+
1
\;z\left [ i-l \right]<r-i+1
z[i−l]<r−i+1时,那么有
z
[
i
]
=
z
[
i
−
l
]
\;z\left [ i \right]\;=z\left [ i-l \right]\;
z[i]=z[i−l]。其中
r
−
i
+
1
\;r-i+1\;
r−i+1表示
s
i
s
i
+
1
.
.
.
s
r
\;s_is_{i+1}...s_r\;
sisi+1...sr的长度。也就是说,如果
z
[
i
−
l
]
\;z\left [ i-l \right]\;
z[i−l]的长度在当前的
b
o
x
box
box内,根据这种相等关系可以直接得出
z
[
i
]
\;z\left [ i \right]\;
z[i]的值,同时这种情况下不需要更新
l
\;l\;
l和
r
\;r\;
r,因为新的
r
\;r\;
r还是在原
b
o
x
\;box\;
box中。
另外一种情况就是
z
[
i
−
l
]
≥
r
−
i
+
1
\;z\left [ i-l \right]≥r-i+1\;
z[i−l]≥r−i+1时,这时候根据已有的区间信息我们仅仅只能得到
s
i
−
l
s
i
−
l
+
1
.
.
.
s
r
−
l
=
=
s
i
s
i
+
1
.
.
.
s
r
\;s_{i-l}s_{i-l+1}...s_{r-l}==s_is_{i+1}...s{r}\;
si−lsi−l+1...sr−l==sisi+1...sr,而对于
r
\;r\;
r之后的元素,由于已经在
b
o
x
\;box\;
box之外,所以我们没法判断其是否与对应的前缀相等,所以这时候仍然需要去枚举一一比对。在这种情况下,如果
r
\;r\;
r之后有相同元素,那么
l
\;l\;
l和
r
\;r\;
r将更新。特别地,考虑
z
[
i
−
l
]
=
=
r
−
i
+
1
\;z\left [ i-l \right]==r-i+1\;
z[i−l]==r−i+1的情况为什么不是和上一种情况一样,此时
z
[
i
−
l
]
\;z\left [ i-l \right]\;
z[i−l]的长度并没有超出
b
o
x
\;box\;
box,这是因为及时没有超出
b
o
x
\;box\;
box,但它已经到了临界范围,对于下一个元素是否相同是未知的,所以需要进行枚举比对。
有了上述设计思想后,实现代码如下:
void get_next(char* ch, int f)
{
int l = 0, r = 0;
z[0] = len;
for (int i = 1; i < len; i++)
{
if (i > r) //对应于第一种情况,此时box不能提供帮助,所以枚举得到z[i]
{
int j = 0;
while (ch[j] == ch[i + j])
{
j++;
}
if (j) //如果存在与前缀相同的后缀,则需要更新box的范围
{
l = i;
r = i + j - 1;
}
z[i] = j;
}
else
{
if (z[i - l] < r - i + 1) //对于第二种情况的第一种情况
{
z[i] = z[i - l];
}
else
{
int j = 1;
while (ch[r + j] == ch[r - i + j]) //枚举box范围外有多少相同元素
{
j++;
}
if (j > 1) //如果box范围外还存在相同元素,更新新的l和r
{
l = i;
r += j - 1;
}
z[i] = r - i + 1;
}
}
}
}
那么该如何应用 Z \;Z\; Z算法呢,只需要将模式串放在文本串之前,然后在计算 z [ i ] \;z\left [ i \right] z[i]的过程中,一旦 i > s . l e n g t h ( ) & & z [ i ] ≥ s . l e n g t h \;i>s.length()\&\&z\left [ i \right]≥s.length\; i>s.length()&&z[i]≥s.length,那么就可以得到文本串中存在模式串的子串。
例题:CF 149E-Martian Strings
题意大概描述的就是给一个文本串
s
\;s\;
s和多个模式串
p
\;p\;
p,问是否能在
s
\;s\;
s中找到两个不重复的连续子串使其组合成为
p
\;p\;
p。(当然这道题也可以利用
k
m
p
\;kmp\;
kmp算法或者
A
C
\;AC\;
AC自动机完成,这里重点为突出
Z
\;Z\;
Z算法的使用。)
算法思想:由于考虑到现在所要匹配的字符串分为了两个部分,所以我们可以分别正向和反向匹配一次,正向匹配记录下每个长度的前缀第一次出现的位置,反向匹配记录下每个长度的后缀第一次出现的位置,然后枚举前缀和后缀,如果二者位置不重合,则可认为存在这样的两个字串构成模式串。
Solution:
#include<bits/stdc++.h>
using namespace std;
const int maxn = 2e5 + 1005;
int m, len, len_s, len_t, ans;
int forward_right[maxn], reverse_left[maxn], z[maxn]; //forward_right记录正向匹配过程中,每一个长度前缀出现的末位置,reverse_left记录记录反向匹配过程中,每一个长度后缀出现的始位置
char s1[maxn], t1[maxn], s2[maxn], t2[maxn];
bool flag;
inline void z_func(char* ch, int f) //f作为标志域,判断是正向匹配还是反向匹配
{
flag = false;
memset(z, 0, sizeof(z));
int l = 0, r = 0;
z[0] = len;
for (int i = 1; i < len; i++)
{
//对应三种情况
if (i > r)
{
int j = 0;
while (ch[j] == ch[i + j])
{
j++;
}
if (j)
{
l = i;
r = i + j - 1;
}
z[i] = j;
}
else
{
if (z[i - l] < r - i + 1)
{
z[i] = z[i - l];
}
else
{
int j = 1;
while (ch[r + j] == ch[r - i + j])
{
j++;
}
if (j > 1)
{
l = i;
r += j - 1;
}
z[i] = r - i + 1;
}
}
if (z[i] >= len_t) //当z[i]值大于模式串的长度时,意味着文本串中有模式串的子串,这时候可以肯定结果,不需要再匹配了
{
ans++;
flag = true;
return;
}
if (i >= len_t && z[i]) //当开始匹配文本串位置的字符时,如果当前位置有与前缀相同部分,则记录下前缀出现的位置
{
if (f)
{
if (!forward_right[z[i]]) //这里是为了保证记录的位置尽可能靠前
{
for (int j = i, t = 1; t <= z[i]; j++, t++)
{
if (forward_right[t])
{
continue;
}
forward_right[t] = j;
}
}
}
else
{
if (!reverse_left[z[i]]) //这里是为了保证记录的位置尽可能靠后
{
for (int j = i, t = 1; t <= z[i]; j++, t++)
{
if (reverse_left[t])
{
continue;
}
reverse_left[t] = len - j + len_t - 1;
}
}
}
}
}
}
int main()
{
scanf("%s%d", s1, &m);
while (m--)
{
memset(forward_right, 0, sizeof(forward_right));
memset(reverse_left, 0, sizeof(reverse_left));
scanf("%s", t1);
len_s = (int)strlen(s1);
len_t = (int)strlen(t1);
if (len_t < 2 || len_t>len_s)
{
continue;
}
strcpy(s2, s1);
strcpy(t2, t1);
len = len_s + len_t;
strcat(t1, s2); //t1表示正向t+s
z_func(t1, 1);
if (flag)
{
continue;
}
reverse(s2, s2 + len_s);
reverse(t2, t2 + len_t);
strcat(t2, s2); //t2表示反向t+s
z_func(t2, 0);
if (forward_right[len_t])
{
ans++;
continue;
}
for (int i = 1; i < len_t; i++)
{
if (forward_right[i] && reverse_left[len_t - i])
{
if (reverse_left[len_t - i] > forward_right[i])
{
ans++;
break;
}
}
}
}
printf("%d\n", ans);
return 0;
}