问题引入

众所周知,\(\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. Java实践:一个简易的http server和client的java源码学习和总结。

    一.基本思路: 1.服务器端通过socket(), 监听在TCP 8080端口,等待客户端来连接. 2.服务器端解析客户端的HTTP请求中的URI值,把本地的目录下指定文件通过java的读取文件的方式 ...

  2. The account that is running SQL Server Setup does not have one or all of the following rights: the right to back up files and directories, the right to manage auditing and the security log and the rig

    安装SQL SERVER 是规则检查提示权限问题 运行secpol.msc,没有Debug program权限,添加即可,如果已加域则要在域策略修改,或退域安装后在加域.

  3. MS SQL自定义函数IsNumeric

    判断字符串是否为纯数字,负数不算.如'00012','54585','1000' SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO CREATE FUN ...

  4. 【原】Java学习笔记023 - 字符串缓冲区_正则表达式

    package cn.temptation; import java.util.Arrays; public class Sample01 { public static void main(Stri ...

  5. java-----理解java的三大特性之多态

    的java提高篇(四)-----理解的java的三大特性之多态 面向对象编程有三大特性:封装,继承,多态. 封装隐藏了类的内部实现机制,可以在不影响使用的情况下改变类的内部结构,同时也保护了数据.对外 ...

  6. 【初学必备】Win10环境下Anaconda安装

    Anaconda集合了python,Spyder,Jupyter notebook及conda-----包管理器与环境管理器(含常用的panda,numpy等),省去单独下载的繁琐步骤,方便使用. 注 ...

  7. LeetCode算法题-Reverse String II(Java实现)

    这是悦乐书的第256次更新,第269篇原创 01 看题和准备 今天介绍的是LeetCode算法题中Easy级别的第123题(顺位题号是541).给定一个字符串和一个整数k,你需要反转从字符串开头算起的 ...

  8. OKR相关4本书,好书3本

    最近几年看过4本OKR相关的书,有3本是4星.其中第一本是最近看的,剩下3本是2017年看的. OKR源自德鲁克和格鲁夫,跟谷歌是天作之合:4星|<这就是OKR> 4星|<OKR实践 ...

  9. WPF中自定义标题栏时窗体最大化处理之WindowChrome

    注意: 本文方法基础是WindowChrome,而WindowChrome在.NET Framework 4.5之后才集成发布的.见:WindowChrome Class 在.NET Framewor ...

  10. docker 安装 mongodb

    1.docker search mongo 2.docker pull mongo 3.//docker run -d --name mymongo -p 27017:27017 -v /home/h ...