扩展KMP(Z函数)详解

一、Z Func

1.1 定义

对于一个长度为n的字符串S。z[i] 表示S与其后缀S[i,n]的**最长公共前缀(LCP)**的长度。

国外一般将计算该数组的算法称为 Z Algorithm,而国内则称其为 扩展 KMP

下面介绍O(N)时间复杂度内求解Zfunc的方法。

其计算方法和Manacher算法极为相似,详见:Manacher(马拉车)算法详解,原理分析-CSDN博客

1.2 暴力算法

std::vector<int> zFunction(const std::string s) {
    int n = s.size();
    std::vector<int> z(n);
    for (int i = 0 ; i < n; ++ i) 
		while (i + z[i] < n && s[z[i]] == s[i + z[i]]) ++ z[i];
    return z;
}

1.3 线性算法

1.3.1 Z-box

类似 Manacher 算法 的加速盒子,Zfunc 的 线性算法也有一个盒子,为当前最靠右的匹配后缀,我们称之为 Z-box

通常用指针[l, r]代表Z-box的左右边界,个人喜欢维护Z-box的起始指针 j,右边界可以通过j + z[j] 获得。

1.3.2 算法流程
  • 当前 Z-box 左端点为 j,需要计算z[i] 的值

  • 如果 i 在 [j, j + z[i]) 内,那么我们可以用 i 在 [j, j + z[i]) 的相对位置,计算出前缀的对应位置 k = i - j

    那么我们可以利用前缀与Z-box的匹配计算出盒内的z 值

    z[i] = std::min(j + z[j] - i, z[i - j]);,二者取min是因为我们只能计算盒内部分

  • 对于盒外部分,暴力扩展:

    while (i + z[i] < n && s[z[i]] == s[i + z[i]]) ++ z[i];

  • 计算完毕后,如果i + z[i] > j + z[j],则说明要更新Z-box

1.3.3 复杂度分析

对于内层 while 循环,如果成功扩展,一定会在后面更新j(因为如果不扩展,说明i + z[i] 根本没有到达盒子右边界)

而 右边界最多移动到 n ,所以while的整体时间复杂度是O(N)

因而我们算法整体时间复杂度为O(N)

1.3.4 代码实现
std::vector<int> zFunction(const std::string s) {
    int n = s.size();
    std::vector<int> z(n + 1);
    z[0] = n;

    for (int i = 1, j = 1; i < n; ++ i) {
        z[i] = std::max(0, std::min(j + z[j] - i, z[i - j]));
        while (i + z[i] < n && s[z[i]] == s[i + z[i]])
            ++ z[i];
        if (i + z[i] > j + z[j])
            j = i;
    }

    return z;
}

二、OJ练习

2.1 模板

原题链接

P5410 【模板】扩展 KMP/exKMP(Z 函数) - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

思路分析

zFunc计算部分就不说了,我们如何线性求p[]?

类似zFunc 的计算,我们维护一个p-Box,代表 字符串 a 的最右匹配后缀

那么同样的,盒内转移,盒外暴力扩展即可

时间复杂度:O(N)

注意开long long

AC代码

#include <bits/stdc++.h>
#include <ranges>
// #define DEBUG
using i64 = long long;

std::vector<int> zFunction(const std::string& s) {
    int n = s.size();
    std::vector<int> z(n + 1);
    z[0] = n;
    for (int i = 1, j = 1; i < n; ++ i) {
        z[i] = std::max(0, std::min(j + z[j] - i, z[i - j]));
        while (i + z[i] < n && s[z[i]] == s[i + z[i]]) ++ z[i];
        if (i + z[i] > j + z[j]) j = i;
    }
    return z;
}

void solve()
{
    std::string a, b;
    std::cin >> a >> b;
    std::vector<int> z = zFunction(b);
    i64 s = 0;
    int n = a.size(), m = b.size();

    for (int i = 0; i < m; ++ i) s ^= (z[i] + 1LL) * (i + 1);
    std::cout << s << '\n';
    
    std::vector<int> p(n + 1);
    s = 0;

    for (int i = 0, j = 0; i < n; ++ i) {
        p[i] = std::max(0, std::min(j + p[j] - i, z[i - j]));
        while (i + p[i] < n && p[i] < m && a[i + p[i]] == b[p[i]]) ++ p[i];
        if (i + p[i] > j + p[j]) j = i;
    }

    for (int i = 0; i < n; ++ i) s ^= (p[i] + 1LL) * (i + 1);
    std::cout << s;
}

int main()
{
#ifdef DEBUG
    freopen("in.txt", "r", stdin);
    freopen("out.txt", "w", stdout);
#endif
    std::ios::sync_with_stdio(false), std::cin.tie(nullptr), std::cout.tie(nullptr);
    int _ = 1;
    // std::cin >> _;
    while (_--)
        solve();
    return 0;
}

2.2 Password

原题链接

CF126B Password

思路分析

很简单,如果答案存在那么它是一个后缀,那么z[i] = n - i

然后只需判断中间是否出现过即可

AC代码

#include <bits/stdc++.h>
#include <ranges>
#define sc scanf
// #define DEBUG
using u32 = unsigned;
using i64 = long long;
using u64 = unsigned long long;
using i128 = __int128;
using PII = std::pair<int, int>;
constexpr int inf32 = 1E9 + 7;
constexpr i64 inf64 = 1e18 + 7;
constexpr int P = 1E9 + 7;
constexpr double eps = 1E-6;

std::vector<int> zFunction(const std::string& s) {
    int n = s.size();
    std::vector<int> z(n + 1);
    z[0] = n;
    for (int i = 1, j = 1; i < n; ++ i) {
        z[i] = std::max(0, std::min(j + z[j] - i, z[i - j]));
        while (i + z[i] < n && s[z[i]] == s[i + z[i]]) ++ z[i];
        if (i + z[i] > j + z[j]) j = i;
    }
    return z;
}

void solve()
{
    std::string s;
    std::cin >> s;
    auto z = zFunction(s);
    int n = s.size();
    std::vector<int> cnt(n + 2);
    int ma = 0, res = 0;
    for (int i = 1; i < n; ++ i) {
        if (z[i] == n - i && (cnt[z[i]] || z[i] < ma))
            res = std::max(res, z[i]);
        ++ cnt[z[i]];   
        ma = std::max(ma, z[i]);
    }
    
    if (res) std::cout << s.substr(0, res);
    else std::cout << "Just a legend";
}

int main()
{
#ifdef DEBUG
    freopen("in.txt", "r", stdin);
    freopen("out.txt", "w", stdout);
#endif
    std::ios::sync_with_stdio(false), std::cin.tie(nullptr), std::cout.tie(nullptr);
    int _ = 1;
    // std::cin >> _;
    while (_--)
        solve();
    return 0;
}
  • 6
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

EQUINOX1

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值