[C++]洛谷:【模板】扩展 KMP(Z 函数) 算法详解

-原题呈现-

[原题]

给定两个字符串 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}

在每轮循环的最后,我们需要维护盒子的右端点,这个过程只需将ri + 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)。


附:KMP算法讲解链接

洛谷:KMP字符串匹配 KMP算法详解

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
KMP算法是一种字符串匹配算法,用于在一个文本串S内查找一个模式串P的出现位置。其核心思想是利用已经匹配的部分来避免重复匹配。 具体实现如下: 1. 预处理模式串P,得到一个next数组。next[i]表示当P[i]与S[j]不匹配时,P[i]应该与S的哪个位置进行匹配,即P[0]~P[next[i]-1]与S[j-next[i]+1]~S[j-1]已经匹配成功。 2. 从文本串S的第一个位置开始匹配,同时用一个指针i记录当前匹配到的模式串位置,用另一个指针j记录当前匹配到的文本串位置。 3. 如果P[i]与S[j]匹配成功,则继续匹配下一个位置。 4. 如果P[i]与S[j]不匹配,则根据next[i]的值将模式串向右移动i-next[i]个位置,同时将i设置为next[i],继续匹配。 5. 如果匹配成功,则返回匹配位置;如果匹配失败,则返回-1。 代码实现如下: ```java public static int kmp(String s, String p) { int[] next = getNext(p); int i = 0, j = 0; while (i < p.length() && j < s.length()) { if (i == -1 || p.charAt(i) == s.charAt(j)) { i++; j++; } else { i = next[i]; } } if (i == p.length()) { return j - i; } else { return -1; } } private static int[] getNext(String p) { int[] next = new int[p.length()]; next[0] = -1; int i = 0, j = -1; while (i < p.length() - 1) { if (j == -1 || p.charAt(i) == p.charAt(j)) { i++; j++; next[i] = j; } else { j = next[j]; } } return next; } ``` 以上代码实现了KMP算法,其中getNext()方法用于计算next数组,kmp()方法用于匹配字符串。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值