固定候选起点和长度,用 LCP 与单调指针求最大可行 K,再用差分统计每个 pair 的 winner 数。
OJ: usaco
题目 ID: 1424
难度:普及+/提高
标签:字符串LCP差分usaco
日期: 2026-07-11 20:58
题意
给定字符串
对每一对
- 枚举所有长度为
的子串; - 在每个长度为
的子串中,找字典序最小的长度为 的子串; - 如果有多个最小值,取最左边的那个;
- 把这些 winner 在原串里的起点加入集合
。
要求对每个
思路
先看一个直接模拟题意的暴力:
/**
* 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-07-11 20:58
* update_at: 2026-07-11 21:00
*/
// brute.cpp:小数据暴力解,用来帮助理解题意并辅助对拍。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 105;
int n;
string s;
long long answer_cnt[MAXN];
bool marked[MAXN];
int compare_sub(int i, int j, int len) {
for (int k = 0; k < len; k++) {
if (s[i + k] < s[j + k]) return -1;
if (s[i + k] > s[j + k]) return 1;
}
return 0;
}
int calc_pair(int big_len, int small_len) {
for (int i = 0; i < n; i++) {
marked[i] = false;
}
for (int start = 0; start + big_len <= n; start++) {
int best_pos = start;
for (int p = start + 1; p + small_len <= start + big_len; p++) {
if (compare_sub(p, best_pos, small_len) < 0) {
best_pos = p;
}
}
marked[best_pos] = true;
}
int cnt = 0;
for (int i = 0; i < n; i++) {
if (marked[i]) cnt++;
}
return cnt;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> s;
for (int big_len = 1; big_len <= n; big_len++) {
for (int small_len = 1; small_len <= big_len; small_len++) {
int cnt = calc_pair(big_len, small_len);
answer_cnt[cnt]++;
}
}
for (int v = 1; v <= n; v++) {
cout << answer_cnt[v] << '\n';
}
return 0;
}暴力枚举每个 K-mer 内的所有长度
换一个方向:不直接问某个
设当前候选串为:
T = S[p..p+L-1]如果某个窗口要让
| 位置 | 阻挡条件 | 原因 |
|---|---|---|
| 左侧 |
更小会赢;相等时最左边会赢 | |
| 右侧 |
只有严格更小时才会抢走 winner |
只需要看最近的两个阻挡位置:
a:左侧最大的阻挡位置;b:右侧最小的阻挡位置。
为了避开左阻挡,可以把窗口起点放在 a+1。这个窗口中最后一个长度
a + 1 + K - L它必须在 b 左边:
a + 1 + K - L < b整理得到:
K <= b + L - a - 2所以固定
L <= K <= maxK贡献一个 winner。
接下来要快速求左右阻挡位置。先预处理 LCP:
lcp[i][j] = S[i..] 和 S[j..] 的最长公共前缀长度倒序转移:
S[i] == S[j] 时,lcp[i][j] = lcp[i+1][j+1] + 1有了 LCP,两个长度为
对固定 p,随着 L 变化,左右阻挡位置可以用单调指针维护。于是所有 p,L 的贡献可以在
最后用 delta_cnt[L][maxK]++ 记录区间 [L,maxK] 的贡献。对每个 L 从大到小扫 K:
winners += delta_cnt[L][K]
answer_cnt[winners]++此时 winners 就是当前 pair (K,L) 的
代码
/**
* 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-07-11 20:58
* update_at: 2026-07-11 21:00
*/
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 3005;
int n;
string s;
int lcp[MAXN][MAXN]; // lcp[i][j] 表示 s[i..] 和 s[j..] 的最长公共前缀长度。
int delta_cnt[MAXN][MAXN];
int left_limit[MAXN], right_limit[MAXN];
long long answer_cnt[MAXN];
// 比较 s[i..i+len-1] 和 s[j..j+len-1]。
// 返回 -1/0/1,分别表示前者更小、相等、前者更大。
int compare_sub(int i, int j, int len) {
int same = lcp[i][j];
if (same >= len) return 0;
if (i + same == n) return -1;
if (j + same == n) return 1;
if (s[i + same] < s[j + same]) return -1;
return 1;
}
void build_lcp() {
for (int i = n - 1; i >= 0; i--) {
for (int j = n - 1; j >= 0; j--) {
if (s[i] == s[j]) {
lcp[i][j] = lcp[i + 1][j + 1] + 1;
}
}
}
}
void solve() {
build_lcp();
for (int p = 0; p < n; p++) {
int b_cand = p + 1;
for (int len = n; len >= 1; len--) {
// 右侧第一个严格小于当前位置子串的位置,会抢走 winner。
while (b_cand < n && compare_sub(p, b_cand, len) <= 0) {
b_cand++;
}
right_limit[len] = min(b_cand + len, n + 1);
}
int a_cand = p - 1;
for (int len = 1; len <= n; len++) {
// 左侧小于或等于当前位置子串的位置,会因为字典序或最左规则抢走 winner。
while (a_cand >= 0 && compare_sub(p, a_cand, len) < 0) {
a_cand--;
}
left_limit[len] = a_cand + 1;
}
for (int len = 1; len <= n; len++) {
if (p + len > n) continue;
int max_k = right_limit[len] - left_limit[len] - 1;
if (max_k > n) max_k = n;
if (max_k >= len) {
delta_cnt[len][max_k]++;
}
}
}
for (int len = 1; len <= n; len++) {
int winners = 0;
for (int k = n; k >= len; k--) {
winners += delta_cnt[len][k];
answer_cnt[winners]++;
}
}
for (int v = 1; v <= n; v++) {
cout << answer_cnt[v] << '\n';
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> s;
solve();
return 0;
}复杂度
预处理 LCP 为
枚举每个位置并用单调指针统计贡献,总复杂度为
最后汇总 delta_cnt 也是
空间复杂度为
总结
本题的关键转化是:从“一个
左侧相等会抢走 winner,右侧相等不会抢走 winner,这是最容易写错的地方。处理好左右阻挡后,每个位置的贡献就是一段连续的窗口长度区间,再用差分统计即可。