问题引入

众所周知,\(\mathrm{KMP}\) 算法是最为经典的单模板字符串匹配问题的线性解法。那么 \(\mathrm{ExKMP}\) 字面意义是 \(\mathrm{KMP}\) 的扩展,那么它是解决什么问题呢?

CaiOJ 1461 【EXKMP】最长共同前缀长度

存在母串 \(S\) 和子串 \(T\) ,设 \(|S| = n, |T| = m\) ,求 \(T\) 与 \(S\) 的每一个后缀的最长公共前缀 \((\mathrm{LCP})\)。

设 \(extend\) 数组, \(extend[i]\) 表示 \(T\) 与 \(S_{i \sim n}\) 的 \(\mathrm{LCP}\) ,对于 \(i \in [1, n]\) 求 \(extend[i]\) 。

\(1 \le m \le n \le 10^6\)

以下的字符串下标均从 \(1\) 开始标号。

算法讲解

本文参考了这位 大佬的讲解

其实可以直接用 \(SA / SAM\) 解决,但是太大材小用了。。。(但似乎不太好做到 \(O(n)\) 有一种是做到 \(O(n) - O(1) \mathrm{RMQ}\) )

对于一般的 \(\mathrm{KMP}\) 只需要求所有 \(extend[i] = m\) 的位置,那么 \(\mathrm{ExKMP}\) 就是需要求出这个 \(extend[i]\) 数组。

举个例子更好理解。

令 \(S = \underline{aaaabaa}, T = \underline{aaaaa}\) 。

S: a a a a b a a
| | | | X
T: a a a a a

我们知道 \(extend[1] = 4\) ,然后计算 \(extend[2]\) ,我们发现重新匹配是很浪费时间的。

由于 \(S_{1 \sim 4} = T_{1\sim 4}\) ,那么 \(S_{2 \sim 4} = T_{2 \sim 4}\) 。

此时我们需要一个辅助的匹配数组 \(next[i]\) 表示 \(T_{i \sim m}\) 与 \(T\) 的 \(\mathrm{LCP}\) 。

我们知道 \(next[2] = 4\) ,那么 \(T_{2 \sim 5} = T_{1 \sim 4} \Rightarrow T_{2 \sim 4} = T_{1 \sim 3}\) 。

所以可以直接从 \(T_4\) 开始和 \(S_5\) 匹配,此时发现会失配,那么 \(extend[2] = 3\) 。

这其实就是 \(\mathrm{ExKMP}\) 的主要思想,下面简述其匹配的过程。

匹配过程

此处假设我们已经得到了 \(next[i]\) 。

当前我们从前往后依次递推 \(extend[i]\) ,假设当前递推完前 \(k\) 位,要求 \(k + 1\) 位。

此时 \(extend[1 \sim k]\) 已经算完,假设之前 \(T\) 能匹配 \(S\) 的后缀最远的位置为 \(p = \max_{i < k} (i + extend[i] - 1)\) ,对应取到最大值的位置 \(i\) 为 \(pos\) 。

S: 1 ... pos ... k k+1 ... p ...

那么根据 \(extend\) 数组定义有 \(S_{pos \sim p} = T_{1 \sim p - pos + 1} \Rightarrow S_{k + 1 \sim p} = T_{k - pos + 2 \sim p -pos + 1}\) 。

令 \(len = next[k - pos + 2]\) ,分以下两种情况讨论。

  1. \(k + len < p\) 。

    S: 1 ... pos ... k k+1 ... k+len k+len+1 ... p ...
    | | | X
    T: 1 ... len len+1 ...

    此时我们发现 \(S_{k + 1 \sim k + len} = T_{1 \sim len}\) 。

    由于 \(next[k - pos + 2] = len\) 所以 \(T_{k + len + pos + 2} \not = T_{len + 1}\) 。

    又由于 \(S_{k + len + 1} = T_{k + len - pos + 2}\) 所以 \(S_{k + len + 1} \not = T_{len + 1}\) 。

    这意味着 \(extend[k + 1] = len\) 。

  2. \(k + len \ge p\)

    S: 1 ... pos ... k k+1 ...  p  p+1   ... ...
    | | | ?
    T: 1 ... ... p-k+2 ... len ...

    那么 \(S_{p + 1}\) 之后的串我们都从未尝试匹配过,不知道其信息,我们直接暴力向后依次匹配即可,直到失配停下来。

    如果 \(extend[k + 1] + k > p\) 要更新 \(p\) 和 \(pos\) 。

next 的求解

前面我们假设已经求出 \(next\) ,但如何求呢?

其实和 \(\mathrm{KMP}\) 是很类似的,我们相当于 \(T\) 自己匹配自己每个后缀的答案,此处需要的 \(next\) 全都在前面会计算过。

和前面匹配的过程是一模一样的。

复杂度证明

下面来分析一下算法的时间复杂度。

  1. 对于第一种情况,无需做任何匹配即可计算出 \(extend[i]\) 。

  2. 对于第二种情况,都是从未被匹配的位置开始匹配,匹配过的位置不再匹配,也就是说对于母串的每一个位置,都只匹配了一次,所以算法总体时间复杂度是 \(O(n)\) 的。

代码解决

注意 \(k + 1 = i\) ,不要弄错下标了。

#include <bits/stdc++.h>

#define For(i, l, r) for (register int i = (l), i##end = (int)(r); i <= i##end; ++i)
#define Fordown(i, r, l) for (register int i = (r), i##end = (int)(l); i >= i##end; --i)
#define Rep(i, r) for (register int i = (0), i##end = (int)(r); i < i##end; ++i)
#define Set(a, v) memset(a, v, sizeof(a))
#define Cpy(a, b) memcpy(a, b, sizeof(a))
#define debug(x) cout << #x << ": " << (x) << endl
#define next Next using namespace std; template<typename T> inline bool chkmin(T &a, T b) { return b < a ? a = b, 1 : 0; }
template<typename T> inline bool chkmax(T &a, T b) { return b > a ? a = b, 1 : 0; } inline int read() {
int x(0), sgn(1); char ch(getchar());
for (; !isdigit(ch); ch = getchar()) if (ch == '-') sgn = -1;
for (; isdigit(ch); ch = getchar()) x = (x * 10) + (ch ^ 48);
return x * sgn;
} void File() {
#ifdef zjp_shadow
freopen ("1461.in", "r", stdin);
freopen ("1461.out", "w", stdout);
#endif
} const int N = 1e6 + 1e3; void Get_Next(char *S, int *next) {
int lenS = strlen(S + 1), p = 1, pos;
next[1] = lenS; // 对于 next[1] 要特殊考虑
while (p + 1 <= lenS && S[p] == S[p + 1]) ++ p;
next[pos = 2] = p - 1; // next[2] 是为了初始化 For (i, 3, lenS) { // 注意此时 k + 1 = i
int len = next[i - pos + 1];
if (len + i < p + 1) next[i] = len; // 对应上面第一种情况
else {
int j = max(p - i + 1, 0); // 找到前面对于 子串 最靠后已经匹配的位置
while (i + j <= lenS && S[j + 1] == S[i + j]) ++ j; // 第二种需要暴力匹配
p = i + (next[pos = i] = j) - 1; // 记得更新 p, pos
}
}
} void ExKMP(char *S, char *T, int *next, int *extend) {
int lenS = strlen(S + 1), lenT = strlen(T + 1), p = 1, pos; while (p <= lenT && S[p] == T[p]) ++ p;
p = extend[pos = 1] = p - 1; // 初始化 extend[1] For (i, 2, lenS) {
int len = next[i - pos + 1];
if (len + i < p + 1) extend[i] = len;
else {
int j = max(p - i + 1, 0);
while (i + j <= lenS && j <= lenT && T[j + 1] == S[i + j]) ++ j;
p = i + (extend[pos = i] = j) - 1;
}
} // 和上面基本一模一样啦
} char S[N], T[N]; int next[N], extend[N]; int main () { File(); scanf ("%s", S + 1);
scanf ("%s", T + 1); Get_Next(T, next);
ExKMP(S, T, next, extend); For (i, 1, strlen(S + 1))
printf ("%d%c", extend[i], i == iend ? '\n' : ' '); return 0; }

一些例题

UOJ #5. 【NOI2014】动物园

题意

给你一个字符串 \(S\) ,定义 \(num\) 数组 --- 对于字符串 \(S\) 的前 \(i\) 个字符构成的子串,既是它的后缀同时又是它的前缀,并且 该后缀与该前缀不重叠 ,将这种字符串的数量记作 \(num[i]\) 。

求 \(\prod_{i = 1}^{|S|} (num[i] + 1) \pmod {10^{9}+7}\)

题解

如果会 \(\mathrm{ExKMP}\) 就是裸题了。

然后考虑对于每个 \(S\) 的后缀 \(i\) 会被算多少遍,其实就是对于以 \([i, \min(2 \times (i - 1), i + next[i] - 1)]\) 为结尾的所有前缀有贡献,那么直接差分即可。

复杂度是 \(O(\sum |S|)\) 的。

代码

前面的板子就不再放了。

const int N = 1e6 + 1e3, Mod = 1e9 + 7;

char str[N]; int num[N], next[N];

int main () {

	File();

	for (int cases = read(); cases; -- cases) {

		scanf ("%s", str + 1); Set(num, 0);
Get_Next(str, next); int n = strlen(str + 1);
For (i, 2, n)
if (next[i])
++ num[i], -- num[min(i * 2 - 1, i + next[i])]; int ans = 1;
For (i, 1, n)
ans = 1ll * ans * ((num[i] += num[i - 1]) + 1) % Mod;
printf ("%d\n", ans); } return 0; }

CF1051E Vasya and Big Integers

题意

给你一个由数字构成的字符串 \(a\) ,问你有多少种划分方式,使得每段不含前导 \(0\) ,并且每段的数字大小在 \([l, r]\) 之间。答案对于 \(998244353\) 取模。

\(1 \le a \le 10^{1000000}, 0 \le l \le r \le 10^{1000000}\)

题解

考虑暴力 \(dp\) ,令 \(dp_i\) 为以 \(i\) 为一段结束的方案数。对于填表法是没有那么好转移的,(因为前导 \(0\) 的限制是挂在前面那个点上)我们考虑刷表法。

那么转移为

\[dp_j = dp_j + dp_i~~\{j~|~a_i \not = 0 \& l \le a_{i \sim j} \le r\}
\]

我们发现 \(dp_i\) 能转移到的 \(j\) 一定是一段连续的区间。

我们就需要快速得到这段区间,首先不难发现 \(j\) 对应的位数区间是可以很快确定的,就是 \([l + |L| - 1, i + |R| - 1]\) 。

但是如果位数一样的话需要多花费 \(O(n)\) 的时间去逐位比较大小。

有什么快速的方法吗?不难想到比较两个数字大小的时候是和字符串一样的,就是 \(\mathrm{LCP}\) 的后面一位。

那么我们用 \(\mathrm{ExKMP}\) 快速预处理 \(extend(\mathrm{LCP})\) 就可以了。

代码

const int N = 1e6 + 1e3, Mod = 998244353;

inline void Add(int &a, int b) {
if ((a += b) >= Mod) a -= Mod;
} char S[N], L[N], R[N]; template<typename T>
inline int dcmp(T lhs, T rhs) {
return (lhs > rhs) - (lhs < rhs);
} inline int Cmp(int l, int r, char *cmp, int *Lcp, int len) {
if (r - l + 1 != len) return dcmp(r - l + 1, len);
return l + Lcp[l] > r ? 0 : dcmp(S[l + Lcp[l]], cmp[Lcp[l] + 1]);
} int lenL, lenR, tmp[N], EL[N], ER[N]; inline bool Check(int x, int y) {
return Cmp(x, y, L, EL, lenL) >= 0 && Cmp(x, y, R, ER, lenR) <= 0;
} int tag[N], dp = 1; int main () { File(); scanf ("%s", S + 1);
int n = strlen(S + 1); scanf ("%s", L + 1); lenL = strlen(L + 1); Get_Next(L, tmp); ExKMP(S, L, tmp, EL);
scanf ("%s", R + 1); lenR = strlen(R + 1); Get_Next(R, tmp); ExKMP(S, R, tmp, ER); tag[1] = Mod - 1;
For (i, 1, n) {
int l, r;
if (S[i] == '0') {
if (L[1] == '0') l = r = i;
else { Add(dp, tag[i]); continue; }
} else {
l = i + lenL - 1; if (!Check(i, l)) ++ l;
r = i + lenR - 1; if (!Check(i, r)) -- r;
}
if (l <= r) Add(tag[l], dp), Add(tag[r + 1], Mod - dp);
Add(dp, tag[i]);
} printf ("%d\n", (dp + Mod) % Mod); return 0; }

ExKMP(Z Algorithm) 讲解的更多相关文章

  1. ACM模板_axiomofchoice

    目录 语法 c++ java 动态规划 多重背包 最长不下降子序列 计算几何 向量(结构体) 平面集合基本操作 二维凸包 旋转卡壳 最大空矩形 | 扫描法 平面最近点对 | 分治 最小圆覆盖 | 随机 ...

  2. ACM 模板库

    Template For ACM 一. 字符串 标准库 sscanf sscanf(const char *__source, const char *__format, ...) :从字符串 __s ...

  3. Python机器学习笔记 异常点检测算法——Isolation Forest

    Isolation,意为孤立/隔离,是名词,其动词为isolate,forest是森林,合起来就是“孤立森林”了,也有叫“独异森林”,好像并没有统一的中文叫法.可能大家都习惯用其英文的名字isolat ...

  4. [转]Python机器学习笔记 异常点检测算法——Isolation Forest

    Isolation,意为孤立/隔离,是名词,其动词为isolate,forest是森林,合起来就是“孤立森林”了,也有叫“独异森林”,好像并没有统一的中文叫法.可能大家都习惯用其英文的名字isolat ...

  5. sklearn异常检测demo

    sklearn 异常检测demo代码走读 # 0基础学python,读代码学习python组件api import time import numpy as np import matplotlib ...

  6. Anomaly Detection

    数据集中的异常数据通常被成为异常点.离群点或孤立点等,典型特征是这些数据的特征或规则与大多数数据不一致,呈现出“异常”的特点,而检测这些数据的方法被称为异常检测. 异常数据根据原始数据集的不同可以分为 ...

  7. csp退役前的做题计划1(真)

    csp退役前的做题计划1(真) 因为我太菜了,所以在第一次月考就会退役,还是记录一下每天做了什么题目吧. 任务计划 [ ] Z算法(Z Algorithm) 9.28 [x] ARC061C たくさん ...

  8. React源码深度解析视频 某课网(完整版)

    <ignore_js_op> [课程介绍]:        React毫无疑问是前端界主流的框架,而框架本身就是热点.课程以讲解React实现原理为主,并在实现过程中讲解这么做的原因,带来 ...

  9. 模板库 ~ Template library

    TOC 建议使用 Ctrl+F 搜索 . 目录 小工具 / C++ Tricks NOI Linux 1.0 快速读入 / 快速输出 简易小工具 无序映射器 简易调试器 文件 IO 位运算 Smart ...

随机推荐

  1. Android 网络框架 Retrofit2

    概述 Retrofit是一个OkHttp网络请求框架的封装库,Retrofit通过注解配置网络参数,可以按照我们的规则去构造实际的HTTP请求,能够灵活设置URL.头部.请求体.返回值等,是目前最优雅 ...

  2. centos服务器如何监控访问ip,并将非法ip通过防火墙禁用

    centos服务器如何监控访问ip,并将非法ip通过防火墙禁用 上周给朋友帮忙,上架了一款小游戏(年年有鱼),项目刚一上线,就遇到了ddos攻击,阿里云连续给出了6次ddos预警提示,服务器一度处于黑 ...

  3. 数据库CRUD操作以及MyBatis的配置使用

    • 业务字段设计 • 数据库创建 • CRUD操作 • MyBatis集成 • 注解和XML定义 • ViewObject和DateTool • 首页开发     • 业务字段设计 实体: name: ...

  4. matlab练习程序(神经网络识别mnist手写数据集)

    记得上次练习了神经网络分类,不过当时应该有些地方写的还是不对. 这次用神经网络识别mnist手写数据集,主要参考了深度学习工具包的一些代码. mnist数据集训练数据一共有28*28*60000个像素 ...

  5. 【Linux】阿里云ECS提示RHSA-2017:3263: curl security update(CentOS 7 更新 curl 为最新版本)

    1.前言 由于 CentOS 7 内置的 curl 和 libcurl 源为较旧的 7.29.0,不支持一些新特性且有安全性问题,所以需要更新一下. 2.开始 1.更新ca-bundle · 首先备份 ...

  6. mean项目的分模块开发

    全文字版: 新建maven工程在,作为父工程用于最后集合使用,该工程不需要src,只需要一个pom.xml文件,规定一下依赖版本之类的,再建一个工具类的工程,不需要放配置文件,和工程中方法接口有关的不 ...

  7. Centos6系列Bond配置方法

    在Windows Server平台因业务需求经常会用到NIC双网卡绑定,同样Linux平台下用于网络负载均衡及网络冗余会用到bond模式. Bond模式:0-6,即7种模式. 模式一:mod=0 ,即 ...

  8. 使用laravel-admin后台sdk报错Failed to load resource: net::ERR_CERT_AUTHORITY_INVALID、Provisional headers are shown

    报错Failed to load resource: net::ERR_CERT_AUTHORITY_INVALID请先确定自己的资源url是否可以确实访问到(地址正确与否.访问权限是否开启等) 若n ...

  9. APACHE SPARK 2.0 API IMPROVEMENTS: RDD, DATAFRAME, DATASET AND SQL

    What’s New, What’s Changed and How to get Started. Are you ready for Apache Spark 2.0? If you are ju ...

  10. 《JAVA程序设计》_第三周学习总结

    20175217吴一凡 一.IDEA学生免费版申请后续 收到这个邮件,就说明你申请成功了,点这里进去就行了 点击接受 在下一个界面登录你之前注册的账号绑定许可证就行了,重新登录你的账号就有了一年的许可 ...