问题引入
众所周知,KMP 算法是最为经典的单模板字符串匹配问题的线性解法。那么 ExKMP 字面意义是 KMP 的扩展,那么它是解决什么问题呢?
存在母串 S 和子串 T ,设 |S|=n,|T|=m ,求 T 与 S 的每一个后缀的最长公共前缀 (LCP)。
设 extend 数组, extend[i] 表示 T 与 Si∼n 的 LCP ,对于 i∈[1,n] 求 extend[i] 。
1≤m≤n≤106
以下的字符串下标均从 1 开始标号。
算法讲解
本文参考了这位 大佬的讲解 。
其实可以直接用 SA/SAM 解决,但是太大材小用了。。。(但似乎不太好做到 O(n) 有一种是做到 O(n)−O(1)RMQ )
对于一般的 KMP 只需要求所有 extend[i]=m 的位置,那么 ExKMP 就是需要求出这个 extend[i] 数组。
举个例子更好理解。
令 S=aaaabaa–––––––––,T=aaaaa––––––– 。
我们知道 extend[1]=4 ,然后计算 extend[2] ,我们发现重新匹配是很浪费时间的。
由于 S1∼4=T1∼4 ,那么 S2∼4=T2∼4 。
此时我们需要一个辅助的匹配数组 next[i] 表示 Ti∼m 与 T 的 LCP 。
我们知道 next[2]=4 ,那么 T2∼5=T1∼4⇒T2∼4=T1∼3 。
所以可以直接从 T4 开始和 S5 匹配,此时发现会失配,那么 extend[2]=3 。
这其实就是 ExKMP 的主要思想,下面简述其匹配的过程。
匹配过程
此处假设我们已经得到了 next[i] 。
当前我们从前往后依次递推 extend[i] ,假设当前递推完前 k 位,要求 k+1 位。
此时 extend[1∼k] 已经算完,假设之前 T 能匹配 S 的后缀最远的位置为 p=maxi<k(i+extend[i]−1) ,对应取到最大值的位置 i 为 pos 。
那么根据 extend 数组定义有 Spos∼p=T1∼p−pos+1⇒Sk+1∼p=Tk−pos+2∼p−pos+1 。
令 len=next[k−pos+2] ,分以下两种情况讨论。
-
k+len<p 。
此时我们发现 Sk+1∼k+len=T1∼len 。
由于 next[k−pos+2]=len 所以 Tk+len+pos+2≠Tlen+1 。
又由于 Sk+len+1=Tk+len−pos+2 所以 Sk+len+1≠Tlen+1 。
这意味着 extend[k+1]=len 。
-
k+len≥p
那么 Sp+1 之后的串我们都从未尝试匹配过,不知道其信息,我们直接暴力向后依次匹配即可,直到失配停下来。
如果 extend[k+1]+k>p 要更新 p 和 pos 。
next 的求解
前面我们假设已经求出 next ,但如何求呢?
其实和 KMP 是很类似的,我们相当于 T 自己匹配自己每个后缀的答案,此处需要的 next 全都在前面会计算过。
和前面匹配的过程是一模一样的。
复杂度证明
下面来分析一下算法的时间复杂度。
-
对于第一种情况,无需做任何匹配即可计算出 extend[i] 。
-
对于第二种情况,都是从未被匹配的位置开始匹配,匹配过的位置不再匹配,也就是说对于母串的每一个位置,都只匹配了一次,所以算法总体时间复杂度是 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;
while (p + 1 <= lenS && S[p] == S[p + 1]) ++ p;
next[pos = 2] = p - 1;
For (i, 3, lenS) {
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;
}
}
}
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;
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] 。
求 ∏|S|i=1(num[i]+1)(mod109+7)
题解
如果会 ExKMP 就是裸题了。
然后考虑对于每个 S 的后缀 i 会被算多少遍,其实就是对于以 [i,min(2×(i−1),i+next[i]−1)] 为结尾的所有前缀有贡献,那么直接差分即可。
复杂度是 O(∑|S|) 的。
代码
前面的板子就不再放了。
CF1051E Vasya and Big Integers
题意
给你一个由数字构成的字符串 a ,问你有多少种划分方式,使得每段不含前导 0 ,并且每段的数字大小在 [l,r] 之间。答案对于 998244353 取模。
1≤a≤101000000,0≤l≤r≤101000000
题解
考虑暴力 dp ,令 dpi 为以 i 为一段结束的方案数。对于填表法是没有那么好转移的,(因为前导 0 的限制是挂在前面那个点上)我们考虑刷表法。
那么转移为
dpj=dpj+dpi {j | ai≠0&l≤ai∼j≤r}
我们发现 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__
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效