-原题呈现-
[原题]
给定两个字符串 a,b,你要求出两个数组:
- b 的 z 函数数组 z,即 b 与 b 的每一个后缀的 LCP 长度。
- b 与 a 的每一个后缀的 LCP 长度数组 p。
对于一个长度为 n 的数组 a,设其权值为 xor(i=1 -> n)(i * ai + 1)。
[输入格式]
两行两个字符串 a,b。
[输出格式]
第一行一个整数,表示 z 的权值。
第二行一个整数,表示 p 的权值。
[输入样例]
aaaabaa
aaaaa
[输出样例]
6
21
[样例解释]
z = {5 4 3 2 1},p = {4 3 2 1 0 2 1}。
[数据范围]
对于第一个测试点,|a|,|b| ≤ 2e3。
对于第二个测试点,|a|,|b| ≤ 2e5。
对于 100% 的数据,1 ≤ |a|,|b| ≤ 2e7,所有字符均为小写字母。
-题意分析-
题目给出两个字符串a和b,需要我们求出b与b的每一个后缀子串的LCP长度及b与a的每一个后缀子串的LCP长度并根据公式求出对应的权值。其中,LCP长度为两个字符串的最大公共前缀长度。
-Z函数和Z-box-
首先,在完成这个任务之前,让我们先来了解一下什么是Z函数和Z-box。
如上图,对于字符串b = "aaabcaaaba",我们可以分别截取他的后缀子串,图中的b'和b''即是其中的两个后缀子串。将这两个后缀子串分别与主串相比较,不难发现他们的LCP长度分别为2和1(即为图中红框和蓝框框出的部分长度)。
而z函数数组则进行如下定义:对于主串b[k](k∈[0, n]),取其后缀子串b'[k](k∈[i, n]),z[i]记录的值即为当前后缀子串的LCP长度。通过这种方式,我们可以将字符串b对应的z[i]函数值全部计算出来,结果如下表所示。
显然,要想获得z函数每一位上的值,我们可以通过双指针的方式进行逐位比较,最终获得匹配的字符个数。但是,这样的暴力写法需要将近o(n^2)的时间复杂度,因此我们需要一个更加优化的方法去实现这个过程。这里,我们采用扩展KMP的算法去实现这个过程。
现在,让我们特别关注一下i = 6的状态。
不难发现,此时的z[i]应该为4。现在,我们用一个长度为当前z[i] = 4的盒子将区间[6, 9]框起来,当我们继续更新z[7]的值时,由于b[1~4]和b[6~9]的部分都是“aaab”,我们不难发现,更新z[2]的过程与更新z[7]的过程是完全一致的,最终都是比较至4号位或9号位的'b'字符时发现不匹配而终止。这样,我们便可以直接使用z[2]的值来更新z[7]。同样地,z[3]的值也可以用于更新z[8],z[4]的值也可以用于更新z[9]。可见,只要当前更新的元素在我们构建出的盒子之内,就有机会直接使用之前已更新的元素去更新当前的元素。这个盒子,我们便将它成为Z-box。而通过构建Z-box,我们可以使用已经计算出的z函数值来加速更新后续的z函数值,即对于每个z[i],都可以通过z[1],z[2],...,z[i - 1]的值来加速更新。这样,整体算法的时间复杂度就被缩减至了o(n)。
我们每次计算完前i - 1个z函数,我们需要不断维护盒子[l, r]的右端点r值,根据上面的分析,对于每个盒子内的i值,都有一个对应点x,它们的特点是:从盒子左端点l到i的偏移量与从1到对应点x的偏移量相等。我们可以得到关系式
b[l, r] = b[1, r - l + 1]
当然,并非所有盒子中的值都可以被直接更新,让我们来观察一下i = 3时的状态。
此时,i正处于i = 2时构建出的范围为[2,3]的盒子中。若根据之前的算法,应当用z[2]的值去更新z[3]的值,但是显然z[2]与z[3]是完全不同的。显然,我们需要对这种可能性进行进一步的讨论。
如上图,z[i - l + 1]表示i - l + 1位置处后缀子串的LCP长度,而这个长度一旦超过了盒子原本的长度,那么i位置的超出部分是否能匹配成功我们是无法直接得出的,因此无法直接使用这个值去更新后续i位置的z函数值。这种情况下,我们仍然需要进行逐位比较去得出i位置的z函数值,但这个值显然大于右端点到i位置的距离即r - i + 1,因此我们可以直接从右端点所在位置开始进行逐位比较。
相反地,如果z[i - 1 + 1]在盒子容纳的范围之内,则可以直接使用对应点的z函数值去更新i位置的z函数值。
讨论完了i在盒子内的情况,那如果i在原来的盒子以外呢?那当然是进行最朴素的逐位比较并更新z函数值喽!
-扩展KMP的代码实现-
接下来,我们将上述过程进行代码实现。
首先,对于i = 1的位置,其LCP长度自然是字符串本身长度,因此我们可以直接进行更新。
接下来,假如我们使用最朴素的逐位比较,双指针分别指向1 + z[i]处和i + z[i]处,我们只需要比较这两个位置即可,代码如下:
//这里用s2表示字符串,n2即为字符串的长度
z[1] = n2;
for (int i = 2; i <= n2; i++)
{
while (s2[1 + z[i]] == s2[i + z[i]])
z[i]++;
}
然后,当i在盒子内时,即i <= r时,z[i]可以由z[i - l + 1]的值直接更新或从盒子右端点处开始逐位比较,因此我们可以在逐位比较前将z[i]的值设为min{z[i - l + 1], r - i + 1}。
在每轮循环的最后,我们需要维护盒子的右端点,这个过程只需将r与i + z[i] - 1(i位置可能的新右端点值)进行比较和更新即可。
优化后的代码如下:
void get_z()
{
z[1] = n2;
ll l, r = 0;
for (int i = 2; i <= n2; i++)
{
if (i <= r)
z[i] = min(z[i - l + 1], r - i + 1);
while (s2[1 + z[i]] == s2[i + z[i]])
z[i]++;
if (r < i + z[i] - 1)
l = i, r = i + z[i] - 1;
}
}
通过这种方式,我们即可以成功获得z函数数组啦!
-扩展KMP在双串匹配中的应用-
题目中还需要我们求出p数组:b与a后缀子串的LCP长度对应数组。实际上,这个过程与以上Z函数的求解过程非常的相似,只不过一个是自己与自己的后缀子串相比较,一个是与另一个字符串的后缀子串相比较。
这个过程的代码有三点值得注意:
(1)由于i = 1时我们并不能直接得出p[1]的值,因此我们直接将i = 1的情况也放入循环中进行逐位比较和计算。
(2)由于是两个串之间的比较,所以我们需要借助之前的z数组,每次将p[i]设为min{z[i - l + 1], r - i + 1}再进行逐位计算。
(3)由于两个串的长度不一定相同,所以我们在比较的过程中需要加上边界条件。
接下来,我们直接来看扩展kmp的代码部分:
void exkmp()
{
ll l, r = 0;
for (int i = 1; i <= n1; i++)
{
if (i <= r)
p[i] = min(z[i - l + 1], r - i + 1);
while (1 + p[i] <= n2 && i + p[i] <= n1 && s2[1 + p[i]] == s1[i + p[i]])
p[i]++;
if (i + p[i] - 1 > r)
l = i, r = i + p[i] - 1;
}
}
-整题代码-
#include <iostream>
#include <string.h>
using namespace std;
typedef long long ll;
const int maxn = 2e7 + 50;
string s1, s2;
int n1, n2;
ll z[maxn], p[maxn];
void get_z()
{
z[1] = n2;
ll l, r = 0;
for (int i = 2; i <= n2; i++)
{
if (i <= r)
z[i] = min(z[i - l + 1], r - i + 1);
while (s2[1 + z[i]] == s2[i + z[i]])
z[i]++;
if (r < i + z[i] - 1)
l = i, r = i + z[i] - 1;
}
}
void exkmp()
{
ll l, r = 0;
for (int i = 1; i <= n1; i++)
{
if (i <= r)
p[i] = min(z[i - l + 1], r - i + 1);
while (1 + p[i] <= n2 && i + p[i] <= n1 && s2[1 + p[i]] == s1[i + p[i]])
p[i]++;
if (i + p[i] - 1 > r)
l = i, r = i + p[i] - 1;
}
}
int main()
{
cin >> s1 >> s2;
n1 = s1.size(), n2 = s2.size();
s1 = ' ' + s1, s2 = ' ' + s2;
get_z();
exkmp();
ll ans1 = 0, ans2 = 0;
for (int i = 1; i <= n2; i++)
ans1 ^= 1LL * i * (z[i] + 1);
for (int i = 1; i <= n1; i++)
ans2 ^= 1LL * i * (p[i] + 1);
cout << ans1 << '\n' << ans2;
return 0;
}
-对KMP与扩展KMP的比较和总结-
(1)应用场景:KMP用于计算两个字符串的最大公共前后缀,而扩展KMP则是计算一个字符串与另一个字符串(或是本身)后缀子串的最长公共前缀。
(2)算法实现:KMP借助next数组的预处理实现指针滞留;扩展KMP借助z函数数组的预处理和Z-box实现新状态的加速更新。
(3)时间复杂度:都是从暴力实现的o(n^2)优化至了o(n)。