把 B 与 A 用分隔符拼成 B#A,一次 Z 函数即得 A 每个后缀与 B 的 LCP,再做长度直方图,每个询问 O(1) 回答;总 O(N+M+Q)。
OJ: roj
题目 ID: 3054
难度:提高
标签:字符串Z函数python
创建: 2026-10-01 12:29
更新: 2026-10-07 13:50
形式化题目
给定长度
即后缀
题面示例:ab,六个后缀 aabcde、abcde、bcde、cde、de、e
的匹配长度依次是
| 起点 |
后缀 | 与 ab 的公共前缀 |
|
|---|---|---|---|
| 1 | aabcde |
a(第 2 位 a b) |
1 |
| 2 | abcde |
ab(已比完 |
2 |
| 3 | bcde |
空 | 0 |
| 4 | cde |
空 | 0 |
| 5 | de |
空 | 0 |
| 6 | e |
空 | 0 |
于是 4 1 1 0 0 一致。
正解
思路
朴素做法的瓶颈。 最直接的办法是对每个起点 aaaa... 时
瓶颈在于信息没有共享:str.find、
单次 KMP 匹配)拿不到它。
把整族 LCP 交给 Z 函数。 Z 函数的定义恰好就是 LCP:
把两个串拼起来,让
对
分隔符
- 它大于所有小写字母、不等于
或 的任何字符,所以匹配不可能「跨过」它把 的 末尾和 的开头接起来; - 更关键的是,它让
:一旦比到 就必然失配, LCP 天然被截断在 的末尾,正好符合题目「最多匹配 个字符」的定义,不需要再写 额外的 min或截断判断。
延续上面的样例,0 0 0 1 2 0 0 0 0,切掉 1 2 0 0 0 0 正是
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | |
|---|---|---|---|---|---|---|---|---|---|
| 字符 | a |
b |
{ |
a |
a |
b |
c |
d |
e |
| 0 | 0 | 0 | 1 | 2 | 0 | 0 | 0 | 0 |
观察重点:下标 bcde 与 ab{aabcde
的首字符都是 a,但第二位 a b 于是停住,
Z 算法为什么线性。 维护当前右端最靠后的一个匹配段(称为 Z 盒)
- 若
,由 Z 盒的对称性, 至少是 ——可以直接借用左侧 对称位置已经算好的值,再从这个下界继续往右比; - 每轮结束时,若
就把盒子右移到 。
盒子右端
从长度到答案:直方图。 拿到 hit,对每个 hit[L_i] += 1,则询问 hit[x](
代码
Python 版:
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-01 12:29
# update_at: 2026-10-01 12:29
import sys
DELIM = ord('z') + 1 # 分隔符:大于所有小写字母,保证匹配不会越过 B 的结尾(长度恒不超过 M)
def z_function(seq: bytes) -> list[int]:
"""Z 函数:z[i] 是 seq 与 seq[i:] 的最长公共前缀长度,均摊 O(|seq|)。"""
z = [0] * len(seq)
left = right = 0 # 目前右端最靠后的匹配段 [left, right)
for i in range(1, len(seq)):
if i < right:
z[i] = min(right - i, z[i - left]) # 借用左端对称位置的答案,再截断到已知段的右端
while i + z[i] < len(seq) and seq[z[i]] == seq[i + z[i]]:
z[i] += 1
if i + z[i] > right:
left, right = i, i + z[i]
return z
def solve() -> None:
data = iter(sys.stdin.buffer.read().split())
n = int(next(data))
m = int(next(data))
q = int(next(data))
a = next(data)
b = next(data)
queries = [int(next(data)) for _ in range(q)]
# 拼成 B + 分隔符 + A,一次 Z 函数就同时给出 A 每个后缀与 B 的匹配长度
z = z_function(b + bytes((DELIM,)) + a)[m + 1:]
hit = [0] * (m + 1) # hit[length] = 匹配长度恰好为 length 的后缀个数
for length in z:
hit[length] += 1
ans = [hit[t] if t <= m else 0 for t in queries] # 匹配长度不会超过 M
print('\n'.join(map(str, ans)))
if __name__ == "__main__":
solve()C++ 版(同一算法):
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-06 16:52
* update_at: 2026-10-06 16:52
*/
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 200005; // A 串长度上限
const int MAXM = 200005; // B 串长度上限
const int MAXS = MAXN + MAXM + 5; // 拼接串长度上限:B + 分隔符 + A
typedef long long ll;
ll n, m, q;
char A[MAXN]; // 被匹配的文本串
char B[MAXM]; // 模式串
char s[MAXS]; // 拼接串 s = B + 分隔符 + A,下标从 1 开始
int z[MAXS]; // z[i] 表示 s 与 s[i..] 的最长公共前缀长度
int hit[MAXM]; // hit[len] 表示匹配长度恰好为 len 的后缀个数
// 计算 s[1..len] 的 Z 数组,均摊 O(len)。
void z_function(int len) {
z[1] = 0;
int left = 1, right = 1; // 当前右端最靠后的匹配段 [left, right)
for (int i = 2; i <= len; i++) {
if (i < right) {
// 借用左侧对称位置的答案,并截断到已知段的右端
z[i] = min(right - i, z[i - left + 1]);
}
// 从下界继续往右逐字符比较
while (i + z[i] <= len && s[z[i] + 1] == s[i + z[i]]) {
z[i]++;
}
if (i + z[i] > right) {
left = i;
right = i + z[i];
}
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m >> q;
cin >> (A + 1);
cin >> (B + 1);
// 拼接:s[1..m] 放 B,s[m+1] 放分隔符,s[m+2..m+1+n] 放 A
for (int i = 1; i <= m; i++) {
s[i] = B[i];
}
s[m + 1] = 'z' + 1; // 大于所有小写字母,保证匹配不会跨过分隔符
for (int i = 1; i <= n; i++) {
s[m + 1 + i] = A[i];
}
z_function(m + 1 + n);
// A 的起点 i 在 s 中的下标是 m+1+i,其 z 值就是它与 B 的最长公共前缀
for (int i = 1; i <= n; i++) {
hit[z[m + 1 + i]]++;
}
// 每个询问 O(1) 回答;匹配长度不会超过 M
for (int i = 1; i <= q; i++) {
ll x;
cin >> x;
if (x >= 0 && x <= m) {
cout << hit[x] << '\n';
} else {
cout << 0 << '\n';
}
}
return 0;
}z_function(seq) 是 Z 算法本体,返回整个 solve 里
z_function(b + bytes((DELIM,)) + a)[m + 1:] 一次完成拼接与切片,切片后下标 hit 循环建直方图,最后一行按询问顺序 t <= m
同时挡住「越界」和「不可能存在的匹配长度」两种情况。
复杂度
- 时间
:拼接串长 ,Z 函数均摊线性;建直方图 ; 个询问 各 。 - 空间
:拼接串与 数组各占 ,直方图占 。
总结
- 题目本质是「求
每个后缀与 的 LCP 的分布」,而不是「 在 中出现在哪里」; 后者( str.find、单次 KMP 匹配)拿不到这族长度。 - 把
放在前面、用不出现在字母表中的分隔符隔开 , 段的 Z 数组就直接给出全部 ;分隔符同时把 LCP 天然截断到 ,与题目定义自动吻合。 - 长度到手后只需一个大小为
的直方图, 个询问各自 ,总复杂度 、空间 。