分段处理 F 块,求兴奋值的最小值、最大值和可达步长。
OJ: usaco
题目 ID: 1323
难度:普及/提高-
标签:字符串分类讨论思维usaco
日期: 2026-07-11 16:26
题意
给定一个长度为 B、E、F。
B表示这条消息由 Bessie 发出。E表示这条消息由 Elsie 发出。F表示发送者被隐藏,可能是B,也可能是E。
把所有 F 替换成 B 或 E 后,定义兴奋值为相邻两条消息由同一头牛发送的次数,也就是 BB 或 EE 的出现次数。
要求输出所有可能的不同兴奋值,按从小到大排列。
思路
先看一个小数据暴力:
cpp
/**
* 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 16:26
* update_at: 2026-07-11 16:28
*/
// brute.cpp:小数据暴力解,用来帮助理解题意并辅助对拍。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 25;
int n;
string s;
string cur;
bool can_score[MAXN];
int calc_score() {
int score = 0;
for (int i = 0; i + 1 < n; i++) {
if (cur[i] == cur[i + 1]) {
score++;
}
}
return score;
}
void dfs_fill(int pos) {
if (pos == n) {
int score = calc_score();
can_score[score] = true;
return;
}
if (s[pos] != 'F') {
cur[pos] = s[pos];
dfs_fill(pos + 1);
return;
}
// 小数据暴力:每个 F 分别枚举成 B 或 E。
cur[pos] = 'B';
dfs_fill(pos + 1);
cur[pos] = 'E';
dfs_fill(pos + 1);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
cin >> s;
cur = s;
dfs_fill(0);
int cnt = 0;
for (int i = 0; i <= n - 1; i++) {
if (can_score[i]) cnt++;
}
cout << cnt << '\n';
for (int i = 0; i <= n - 1; i++) {
if (can_score[i]) {
cout << i << '\n';
}
}
return 0;
}暴力把每个 F 都枚举成 B 或 E,再统计相邻相同对。它的复杂度和 F 的数量指数相关,只能用于小数据验证。
满分做法不需要枚举每一种字符串。我们只要求所有可能的兴奋值,可以先求:
min_score:最小可能兴奋值。max_score:最大可能兴奋值。step:可达值之间的间隔,可能是1或2。
没有 F 的相邻已知字符
如果相邻两个字符都不是 F,那么这条边已经确定:
- 相同:
min_score和max_score都加1。 - 不同:都不加。
内部 F 段
考虑一段连续 F,左右两端都有已知字符:
text
L F F ... F R设这段里有 len 个 F,那么这里一共有 len + 1 条相邻边。
为了最大化兴奋值:
- 如果
,可以全部填成同一个字符,贡献 len + 1。 - 如果
,至少有一次变化,最大贡献 len。
为了最小化兴奋值,要尽量交替填写。是否能做到贡献为 0,取决于边数奇偶和左右端点是否不同:
text
边数奇偶 == (L != R) 时,最小贡献为 0
否则最小贡献为 1内部 F 段的可达贡献会隔 2 变化,因为每多制造一对相邻相同,通常要减少一对变化。
贴边 F 段
如果连续 F 段在字符串开头或结尾:
text
F F ... F R
L F F ... F它有一个自由端,可以让贡献从 0 到这段边数之间的每个整数都可达。因此只要首尾有 F,整体可达兴奋值就会变成连续区间,
否则所有变化都来自内部 F 段,最终只会隔 2 取值,
最后从 min_score 开始,每次加 step,一直输出到 max_score。
代码
cpp
/**
* 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 16:26
* update_at: 2026-07-11 16:28
*/
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 200005;
int n;
string s;
int possible_values[MAXN];
int possible_cnt;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
cin >> s;
int min_score = 0;
int max_score = 0;
bool edge_free = (s[0] == 'F' || s[n - 1] == 'F');
for (int i = 0; i + 1 < n; i++) {
if (s[i] != 'F' && s[i + 1] != 'F' && s[i] == s[i + 1]) {
min_score++;
max_score++;
}
}
int i = 0;
while (i < n) {
if (s[i] != 'F') {
i++;
continue;
}
int l = i;
while (i < n && s[i] == 'F') {
i++;
}
int r = i - 1;
int len = r - l + 1;
bool has_left = (l > 0);
bool has_right = (r + 1 < n);
if (has_left && has_right) {
int edge_cnt = len + 1;
int need_odd_change = (s[l - 1] != s[r + 1]);
// 最大值:两端相同可全相同,两端不同至少有一次变化。
max_score += edge_cnt - need_odd_change;
// 最小值:尽量交替;若边数奇偶不匹配,就必须留下一个相等相邻对。
if (edge_cnt % 2 != need_odd_change) {
min_score++;
}
} else {
// 贴边的 F 段有一个自由端,可以让相等对数量从 0 到边数任意变化。
int edge_cnt = len;
if (!has_left && !has_right) {
edge_cnt = len - 1;
}
max_score += edge_cnt;
}
}
int step = edge_free ? 1 : 2;
possible_cnt = 0;
for (int x = min_score; x <= max_score; x += step) {
possible_values[++possible_cnt] = x;
}
cout << possible_cnt << '\n';
for (int i = 1; i <= possible_cnt; i++) {
cout << possible_values[i] << '\n';
}
return 0;
}复杂度
扫描字符串和连续 F 段都是线性的。
时间复杂度为
总结
本题不要枚举每个 F 的具体取值,而是按连续 F 段统计它对兴奋值范围的影响。
内部段只会改变奇偶,贴边段能打破奇偶限制,让整个区间都可达。