ExKMP(Z Algorithm) 讲解

问题引入

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

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

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

extend 数组, extend[i] 表示 TSinLCP ,对于 i[1,n]extend[i]

1mn106

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

算法讲解

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

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

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

举个例子更好理解。

S=aaaabaa_,T=aaaaa_

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

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

由于 S14=T14 ,那么 S24=T24

此时我们需要一个辅助的匹配数组 next[i] 表示 TimTLCP

我们知道 next[2]=4 ,那么 T25=T14T24=T13

所以可以直接从 T4 开始和 S5 匹配,此时发现会失配,那么 extend[2]=3

这其实就是 ExKMP 的主要思想,下面简述其匹配的过程。

匹配过程

此处假设我们已经得到了 next[i]

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

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

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

那么根据 extend 数组定义有 Sposp=T1ppos+1Sk+1p=Tkpos+2ppos+1

len=next[kpos+2] ,分以下两种情况讨论。

  1. k+len<p

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

    此时我们发现 Sk+1k+len=T1len

    由于 next[kpos+2]=len 所以 Tk+len+pos+2Tlen+1

    又由于 Sk+len+1=Tk+lenpos+2 所以 Sk+len+1Tlen+1

    这意味着 extend[k+1]=len

  2. k+lenp

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

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

    如果 extend[k+1]+k>p 要更新 ppos

next 的求解

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

其实和 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]

i=1|S|(num[i]+1)(mod109+7)

题解

如果会 ExKMP 就是裸题了。

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

复杂度是 O(|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 取模。

1a101000000,0lr101000000

题解

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

那么转移为

dpj=dpj+dpi  {j | ai0&laijr}

我们发现 dpi 能转移到的 j 一定是一段连续的区间。

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

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

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

那么我们用 ExKMP 快速预处理 extend(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; }

__EOF__

本文作者zjp_shadow
本文链接https://www.cnblogs.com/zjp-shadow/p/10139818.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   zjp_shadow  阅读(2149)  评论(0编辑  收藏  举报
编辑推荐:
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
点击右上角即可分享
微信分享提示