枚举最终保留的长度 3 子串,中间必须是 O,再统计左右端点替换次数。
OJ: usaco
题目 ID: 1277
难度:普及-
标签:字符串枚举usaco
日期: 2026-07-11 17:10
题意
给定若干个只包含 M 和 O 的字符串。
一次操作可以:
- 把第一个或最后一个字符翻转成另一种字符;
- 删除第一个或最后一个字符。
问每个字符串最少多少次操作可以变成 "MOO",不可能则输出 -1。
思路
先看一个真实模拟操作的 BFS 暴力:
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 17:10
* update_at: 2026-07-11 17:11
*/
// brute.cpp:小数据暴力解,用来帮助理解题意并辅助对拍。
#include <bits/stdc++.h>
using namespace std;
char opposite_char(char c) {
if (c == 'M') {
return 'O';
}
return 'M';
}
int bfs_solve(string start) {
queue<string> q;
map<string, int> dis;
q.push(start);
dis[start] = 0;
while (!q.empty()) {
string cur = q.front();
q.pop();
int d = dis[cur];
if (cur == "MOO") {
return d;
}
int len = (int)cur.size();
if (len == 0) {
continue;
}
string nxt;
// 操作 1:翻转第一个字符。
nxt = cur;
nxt[0] = opposite_char(nxt[0]);
if (dis.find(nxt) == dis.end()) {
dis[nxt] = d + 1;
q.push(nxt);
}
// 操作 1:翻转最后一个字符。
nxt = cur;
nxt[len - 1] = opposite_char(nxt[len - 1]);
if (dis.find(nxt) == dis.end()) {
dis[nxt] = d + 1;
q.push(nxt);
}
// 操作 2:删除第一个字符。
nxt = cur.substr(1);
if (dis.find(nxt) == dis.end()) {
dis[nxt] = d + 1;
q.push(nxt);
}
// 操作 2:删除最后一个字符。
nxt = cur.substr(0, len - 1);
if (dis.find(nxt) == dis.end()) {
dis[nxt] = d + 1;
q.push(nxt);
}
}
return -1;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int q;
cin >> q;
while (q--) {
string s;
cin >> s;
cout << bfs_solve(s) << '\n';
}
return 0;
}这个暴力把每个字符串看成一个状态,用 BFS 尝试四种操作。它可以直接验证小数据的最短操作数,但状态数不适合满分数据。
满分做法从最终形态入手。
最终要得到长度为 3 的字符串。由于删除只能从两端进行,最后留下来的 3 个字符一定是原字符串中的一个连续长度 3 子串。
设这个子串是:
text
s[i - 1], s[i], s[i + 1]中间字符 s[i] 最后要成为 "MOO" 的第二个字符。可是当只剩 3 个字符时,中间字符不能被替换,所以它必须原本就是 O。
于是枚举所有内部位置 i,只考虑 s[i] == 'O' 的情况。
选择这个子串时:
- 删除左右多余字符需要
n - 3次; - 如果
s[i - 1] != 'M',左端要替换一次; - 如果
s[i + 1] != 'O',右端要替换一次。
取所有候选的最小值即可。没有候选时输出 -1。
代码
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 17:10
* update_at: 2026-07-11 17:11
*/
#include <bits/stdc++.h>
using namespace std;
const int INF = 1e9;
int solve(string s) {
int n = (int)s.size();
if (n < 3) {
return -1;
}
int ans = INF;
// 枚举最终保留下来的长度为 3 的子串的中间位置。
for (int i = 1; i <= n - 2; i++) {
if (s[i] != 'O') {
continue;
}
int cur = n - 3; // 删除左右多余字符
if (s[i - 1] != 'M') {
cur++;
}
if (s[i + 1] != 'O') {
cur++;
}
if (cur < ans) {
ans = cur;
}
}
if (ans == INF) {
return -1;
}
return ans;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int q;
cin >> q;
while (q--) {
string s;
cin >> s;
cout << solve(s) << '\n';
}
return 0;
}复杂度
每个字符串只扫描一遍。
单个询问时间复杂度为
总结
本题不要从操作序列硬搜满数据,而要观察最终留下来的结构。
“只能删两端”说明最终三个字符来自连续子串;“只能改两端”说明中间字符必须已经是 O。有了这两个限制,枚举中间位置即可。