【字符串】【P5830】 【模板】失配树
【字符串】【P5830】 【模板】失配树
Description
给定一个长度为 的字符串 ,有 次询问,每次询问给定 的两个前缀,求它们的最长公共 border
的长度。
最长公共 border
的含义为,对于一个字符串 ,设其 Border
集合为所有既是 的前缀子串又是 的后缀子串的集合,两个字符串的最长公共 border
为两个字符串的 Border
集合的交集中长度最长的字符串。
Limitations
Solution
注意,这篇题解不是这个模板的标准做法,也不是最简单的做法。
两个前缀的最长公共 border 即为他们在 border 树上的 LCA
因为刚起床就被 fa姐姐
拉来验题,脑袋昏昏忘记了这个结论,只能再口胡一个铁憨憨做法。
注意到所求的 border
一定既是第一个字符串的后缀,又是第二个字符串的后缀,因此一定是两个字符串的公共后缀 ,同时注意到由于这两个字符串的前缀是相同的,所以如果一个字符串 既是其中任意一个串的 border
,又是两个串的公共后缀,那么它一定是两个串的公共 border
。并且这个条件显然也是必要条件,因此我们在求出两串的 lcp
以后只需要在其中任意一个串上找到其最长的长度不超过 lcp
长度的 border
,那么该串即为两串的最长公共 border
。
假设我们已经求出了两串的 lcp
长度,那么问题就只剩下对一个字符串求其最长的长度不超过某数的 border
。
我们考虑对每个前缀,将它向它的最长 border
连一条边,那么显然这个图有 个节点, 条边,又因为这个图是联通的,根据树的判定定理,这个图是一棵树,若规定 是这棵树的根,数学归纳可得每个节点的父节点为该节点所代表的前缀的最长 border
。因为一个节点的 border
显然比该节点的长度小,所以任何一个节点到根所在的链上,若将节点按深度从小到大排列,则其所代表的前缀长度一定是单调递增的。因此我们只需要对整棵树进行 dfs
,同时用一个栈维护当前节点到根的链,然后在栈里二分即可找到所求的串。
求 border
的方法见 【P3375】KMP字符串匹配。
而求两个前缀的 lcp
,可以对原串建立一个 SAM
,两个前缀在 parent
树上所对应节点的 LCA
即为他们的 lcp
。也可以将原串反过来,转化为求两个后缀的最长公共前缀,求出 SA
后用 height
数组解决。
但是扶苏既不愿意将原串反过来求 SA
在写个 ST
,也担心毒瘤出题人卡了空间以后 SAM
建出来会爆空间,因此扶苏选择了 二分+hash 求出其 lcp
。
显然公共后缀的长度满足二分性,因此只要选择一个满足前缀可减性的 hash
函数就可以 check 了。
考虑时间复杂度:二分求 lcp
的复杂度是 ,在 border
树上二分的复杂度是 ,因此总时间复杂度 。
Code
本来扶苏写了个四模数 hash
,然后被卡常了就尝试减少模数个数,最后发现单模数就可以了(雾
#include <cstdio>
#include <vector>
#include <algorithm>
const int maxh = 4;
const int maxm = 100005;
const int maxn = 1000005;
const int MOD[] = {998244353, 1000000007, 1000000009, 1145141};
int n, m, top = -1;
char S[maxn];
int border[maxn], ans[maxm], stk[maxn];
std::vector<int> son[maxn], query[maxn];
struct HASH {
int md;
ll hash[maxn], inv[maxn];
ll mpow(const int a, int d, const int p) {
ll ret = 1, tmp = a;
while (d) {
if (d & 1) {
(ret *= tmp) %= p;
}
(tmp *= tmp) %= p;
d >>= 1;
}
return ret;
}
void build(const int x) {
md = x;
ll tmp = 1, iv = mpow(100, x - 2, x);
inv[0] = 1;
for (int i = 1; i <= n; ++i) {
hash[i] = (hash[i - 1] + (S[i] - 'a') * tmp) % md;
inv[i] = inv[i - 1] * iv % md;
(tmp *= 100) %= md;
}
}
bool check(const int x, const int y, const int len) {
ll h1 = (hash[x] - hash[x - len]) * inv[x - len] % md, h2 = (hash[y] - hash[y - len]) * inv[y - len] % md;
if (h1 < 0) h1 += md;
if (h2 < 0) h2 += md;
if (h1 != h2) {
return false;
} else {
return true;
}
}
};
HASH h[maxh];
int ReadStr(char *p);
void dfs(const int u);
int main() {
freopen("1.in", "r", stdin);
n = ReadStr(S);
for (int i = 0; i < maxh; ++i) {
h[i].build(MOD[i]);
}
for (int i = 2, j = 0; i <= n; ++i) {
while (j && (S[j + 1] != S[i])) {
j = border[j];
}
if (S[j + 1] == S[i]) {
++j;
}
son[border[i] = j].push_back(i);
}
son[0].push_back(1);
qr(m);
for (int p, q, Ans, i = 1; i <= m; ++i) {
p = q = Ans = 0; qr(p); qr(q);
for (int l = 1, r = std::min(p, q) - 1, mid = (l + r) >> 1; l <= r; mid = (l + r) >> 1) {
bool flag = true;
for (int i = 0; i < maxh; ++i) if ((flag = h[i].check(p, q, mid)) == false) {
break;
}
if (flag) {
l = (Ans = mid) + 1;
} else {
r = mid - 1;
}
}
ans[i] = Ans;
query[std::min(p, q)].push_back(i);
}
dfs(0);
for (int i = 1; i <= m; ++i) {
qw(ans[i], '\n', true);
}
return 0;
}
int ReadStr(char *p) {
auto beg = p;
do *(++p) = IPT::GetChar(); while ((*p >= 'a') && (*p <= 'z'));
*p = 0;
return p - beg - 1;
}
void dfs(const int u) {
stk[++top] = u;
for (auto v : query[u]) {
int w = ans[v]; ans[v] = 0;
for (int l = 1, r = top, mid = (l + r) >> 1; l <= r; mid = (l + r) >> 1) if (stk[mid] <= w) {
ans[v] = stk[mid];
l = mid + 1;
} else {
r = mid - 1;
}
}
for (auto v : son[u]) {
dfs(v);
}
--top;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· .NET10 - 预览版1新功能体验(一)