问题引入

众所周知,\(\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. MVP模式, 开源库mosby的使用及代码分析

    Android中的构架模式一直是一个很hot的topic, 近年来Architecture components推出之后, MVVM异军突起, 风头正在逐渐盖过之前的MVP. 其实我觉得MVP还是有好 ...

  2. DVWA 黑客攻防演练(六)不安全的验证码 Insecure CAPTCHA

    之前在 CSRF 攻击 的那篇文章的最后,我觉得可以用验证码提高攻击的难度. 若有验证码的话,就比较难被攻击者利用 XSS 漏洞进行的 CSRF 攻击了,因为要识别验证码起码要调用api,跨域会被浏览 ...

  3. DVWA 黑客攻防演练(四)文件包含 File Inclusion

    文件包含(file Inclusion)是一种很常见的攻击方式,主要是通过修改请求中变量从而访问了用户不应该访问的文件.还可以通过这个漏洞加载不属于本网站的文件等.下面一起来看看 DVWA 中的文件包 ...

  4. C#微信支付对接

    c#版在pc端发起微信扫码支付   主要代码: /** * 生成直接支付url,支付url有效期为2小时,模式二 * @param productId 商品ID * @return 模式二URL */ ...

  5. CFS调度器(1)-基本原理

    首先需要思考的问题是:什么是调度器(scheduler)?调度器的作用是什么?调度器是一个操作系统的核心部分.可以比作是CPU时间的管理员.调度器主要负责选择某些就绪的进程来执行.不同的调度器根据不同 ...

  6. 新的编辑工具IDE

    因为最近一段时间前端都是用EXTJS开发,之前用vs2012来编辑extjs,感觉缺少智能感应,很不方便.后来发现一款工具Visual Studio Code,非常棒. VS Code是免费,轻量,跨 ...

  7. centos7源码包安装Mongodb,并设置开机自启动

    1.下载源码包 curl -O https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-3.2.12.tgz 2.解压 放到 /usr/local/ ...

  8. Enterprise architect 类图加时序图

    原文地址:https://segmentfault.com/a/1190000005639047#articleHeader2 新建一个Project 没什么好说的,“文件-新建项目”,然后选择保存位 ...

  9. Business Intelligence Tools We Recommend 1/4 – Metabase

    May 24, 2018 by Arturs Oganesyan-Peel BI is useful. It’s pretty. But it never really matters unless ...

  10. PHP跨域jsonp方式

    <?php header('Access-Control-Allow-Origin:*');//注意!跨域要加这个头 上面那个没有 $arr = array ('a'=>1,'b'=> ...