固定子串左端点,用 Z 数组一次求出该段所有前缀匹配长度,把第二个 A 的起点转成连续右端点区间,按偏移递增扫区间并去重,O(n²) 计数 A+B+A 子串。
OJ: roj
题目 ID: 1469
难度:提高
标签:字符串Z函数python
创建: 2026-09-30 13:52
更新: 2026-10-07 11:01
形式化题目
给定小写字母串
计数规则有两层,是本题的关键约束:
- 位置不同算不同子串:起止下标
不同,就是两个答案; - 位置相同但拆分不同只算一个:同一段
只要存在一种合法拆分就计 1 次,不管它有多少种拆法。
于是答案就是"存在合法拆分"的下标对
样例 1:a+aaa+a),也可以拆成 aa+a+aa),按规则只算 1 个。
样例 2:
| 区间 | 长度 | 一种拆分 |
验证 |
|---|---|---|---|
| 5 | ab + c + ab |
前后两段 ab 相同 |
|
| 8 | ab + cabc + ab |
前后两段 ab 相同 |
|
| 9 | abc + abc + abc |
前后两段 abc 相同 |
|
| 5 | bc + a + bc |
前后两段 bc 相同 |
|
| 8 | bc + abca + bc |
前后两段 bc 相同 |
|
| 5 | ca + b + ca |
前后两段 ca 相同 |
|
| 5 | ab + c + ab |
与 |
|
| 5 | bc + a + bc |
与 |
正解
思路
朴素做法:四重枚举。 枚举
第一步:把拆分条件翻译成一个 LCP 条件。 固定子串左端点
,即 ; ,中间那段长度是 ,所以 (顺带保证两段 不重叠); - 两段
逐字符相同,即 ,其中 表示 与 的最长公共前缀。
记
于是固定 i > k)且
第二步:一个偏移贡献一整段右端点。 右端点
两端的含义分别是"
第三步:同一右端点可能被多个偏移产生,求区间并。 对固定
去重可以线性完成:区间左端 covered 记录已覆盖的最右偏移;遇到新区间 covered 推到
样例 1 用这套流程逐个左端点走一遍(
| 左端点 |
有效偏移 |
新增偏移 | 小计 |
|---|---|---|---|
| 1 | 1;2;0(已被覆盖) | 3 | |
| 2 | 1;1 | 2 | |
| 3 | 1 | 1 | |
| 4 | 无(只剩 2 个字符,凑不出 |
— | 0 |
| 5 | 无(只剩 1 个字符) | — | 0 |
| 合计 | 6 |
关键看
第四步:用 Z 数组一次求出全部
对每个左端点各做一次 Z 数组 + 一次区间扫描,两步都是 i > k、zi >= k),并不需要知道
代码
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-09-30 13:54
# update_at: 2026-09-30 13:54
import sys
def union_of_ends(seg: bytes, k: int) -> int:
"""以 seg[0] 为子串左端点时,能凑出多少个不同右端点的 A+B+A 子串。
第二个 A 起点偏移 i 满足 lcp(0,i) >= k 且 i >= k+1(B 非空)时,右端点
落在连续区间 [i+k-1, i+min(lcp,i-1)-1];区间左端点随 i 递增,顺序扫一遍
求区间并,同一右端点(同一子串)就只数一次。
"""
m = len(seg)
z = [0] * m # z[i] = lcp(seg, seg[i:]),经典 Z 数组
box_l = box_r = 0 # Z 盒:seg[box_l..box_r) 与开头同段
covered = -1 # 区间并已覆盖到的最右偏移
ans = 0
for i in range(1, m):
zi = 0
if i < box_r:
zi = box_r - i
v = z[i - box_l]
if v < zi:
zi = v # 没顶到盒边:z[i] 已精确,免去逐字符比较
else:
# 顶到盒边:从盒边界继续向后逐字符延伸
while i + zi < m and seg[zi] == seg[i + zi]:
zi += 1
else:
while i + zi < m and seg[zi] == seg[i + zi]: # 盒外从头求 lcp
zi += 1
if i + zi > box_r:
box_l, box_r = i, i + zi
# 偏移 i 贡献的右端点区间:i>k 即 B 至少 1 个字符,zi>=k 即 |A|>=k
if i > k and zi >= k:
u = i - 1 if i - 1 < zi else zi # min(lcp, i-1):B 不能盖住第二个 A
b = i + k - 1 # 区间左端
e = i + u - 1 # 区间右端
if e > covered:
if b > covered:
ans += e - b + 1 # 整段新区间
else:
ans += e - covered # 只有尾巴是新的
covered = e
z[i] = zi # 供更后面的 Z 盒取用
return ans
def solve() -> None:
data = iter(sys.stdin.buffer.read().split())
text = next(data)
k = int(next(data))
print(sum(union_of_ends(text[start:], k) for start in range(len(text))))
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-05 02:59
* update_at: 2026-10-05 02:59
*/
// main.cpp:固定左端点,用 Z 数组一次求出该段全部 lcp,再按偏移递增扫连续右端点区间求并,O(n^2)。
#include <iostream>
#include <string>
using namespace std;
const int MAXN = 15005;
typedef long long ll;
char s[MAXN]; // 题目给定的小写字母串,下标从 1 开始,s[1..n] 有效
ll n;
ll k;
ll z[MAXN]; // z[i] = lcp(s[l..], s[l+i..]),下标是相对当前左端点 l 的偏移,每换一个 l 全部重算
// 统计以 l 为左端点的合法 A+B+A 子串个数(同一子串只算一次)。
// 第二个 A 的起点偏移为 i 时,合法的 |A| 是一段连续整数,于是右端点也落在连续区间里;
// 偏移 i 从小到大递增时区间左端也递增,用 covered 记录已覆盖的最右偏移就能线性去重。
ll count_left(ll l) {
ll m = n - l + 1; // 当前段 s[l..n] 的长度
ll box_l = 0; // Z 盒:[box_l, box_r) 与 [l, l+box_r-box_l) 逐字符相同
ll box_r = 0;
ll covered = -1; // 区间并已经覆盖到的最右偏移
ll res = 0;
for (ll i = 1; i < m; i++) {
ll zi = 0;
if (i < box_r) {
zi = box_r - i;
ll v = z[i - box_l];
if (v < zi) {
zi = v; // 没顶到盒边:z[i] 已精确,不必逐字符比较
} else {
while (l + i + zi <= n && s[l + zi] == s[l + i + zi]) zi++; // 顶到盒边,继续向后延伸
}
} else {
while (l + i + zi <= n && s[l + zi] == s[l + i + zi]) zi++; // 盒外从头求 lcp
}
if (i + zi > box_r) {
box_l = i;
box_r = i + zi;
}
// i > k 保证 |B| >= 1,zi >= k 保证 |A| >= k,否则该偏移无合法拆分
if (i > k && zi >= k) {
ll upper = zi < i - 1 ? zi : i - 1; // min(lcp, i-1):两个 A 不能重叠
ll b = i + k - 1; // |A| 取最小 k 时的右端点偏移
ll e = i + upper - 1; // |A| 取最大时的右端点偏移
if (e > covered) {
if (b > covered) {
res += e - b + 1; // 整段区间都是新的
} else {
res += e - covered; // 只有尾部是新的,前面已被别的偏移数过
}
covered = e;
}
}
z[i] = zi; // 写在最后,供更后面的位置复用 Z 盒
}
return res;
}
void read_input() {
string text;
cin >> text >> k;
n = text.size();
for (ll i = 1; i <= n; i++) {
s[i] = text[i - 1];
}
}
void solve() {
ll ans = 0;
for (ll l = 1; l <= n; l++) {
ans += count_left(l);
}
cout << ans << endl;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
read_input();
solve();
return 0;
}union_of_ends(seg, k) 就是上面四步的合体:内层求 covered 做区间并;外层对每个左端点调用一次并累加。注意 z[i] = zi 必须写在循环末尾,供后面位置的 Z 盒复用。
复杂度
- 时间
:每个左端点一次 Z 数组 、一次区间扫描 ,全部左端点合计 ; 时约 次基本操作,字符比较最坏同阶(Z 盒只保证每段线性、不退化到 ,消不掉这层平方)。本地全量 10 组数据实测最长约 16s, check_sample.py本地限时放宽到 20s/测例后全部通过;常数主要来自 Python 的逐字符比较。 - 空间
:每个左端点只需一个长度为 的 Z 数组和 个标量,用完即弃。
总结
- 计数规则"同一位置只算一个"决定了正确模型是下标对
的存在性,而不是拆分个数——多个拆分产生的右端点必须取并集。 - 固定左端点后,"是否存在合法拆分"被翻译成
的单个不等式: 且 时,偏移 贡献连续区间 。 - 区间左端随偏移递增,一次带
covered的扫描即可去重,代价线性;全部由 Z 数组线性求出,于是朴素的 (哈希后 )被压到 、空间 。