先由最终连续感染段反推全局最多传播了多少晚,再把每段连续 1 按单个初始感染点最多覆盖的长度分组计数。
OJ: luogu
题目 ID: P9975
难度:普及+/提高
标签:思维字符串贪心
日期: 2026-06-20 11:26
同题版本
本题对应的 USACO 版本及解析:
思路
先看一个最直接的小数据暴力:
#include <bits/stdc++.h>
using namespace std;
int n;
string target_state;
string spread_once(const string &cur) {
string nxt = cur;
for (int i = 0; i < n; i++) {
if (cur[i] == '1') {
if (i - 1 >= 0) {
nxt[i - 1] = '1';
}
if (i + 1 < n) {
nxt[i + 1] = '1';
}
}
}
return nxt;
}
bool can_reach(const string &start) {
string cur = start;
// 最多扩散 n 次后一定稳定。
for (int day = 0; day <= n; day++) {
if (cur == target_state) {
return true;
}
string nxt = spread_once(cur);
if (nxt == cur) {
if (nxt == target_state) {
return true;
}
return false;
}
cur = nxt;
}
return false;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
cin >> target_state;
int ans = n;
// 小数据暴力:枚举所有初始感染状态。
for (int mask = 1; mask < (1 << n); mask++) {
string start(n, '0');
int cnt = 0;
for (int i = 0; i < n; i++) {
if ((mask >> i) & 1) {
start[i] = '1';
cnt++;
}
}
if (cnt >= ans) {
continue;
}
if (can_reach(start)) {
ans = cnt;
}
}
cout << ans << '\n';
return 0;
}brute.cpp 枚举所有初始感染状态,再模拟每天传播,看看能不能在某个时刻得到目标状态。
这个做法很贴近题意,但状态数是 2^n,只能用于非常小的数据。
第一步:按连续 1 分段看
最终状态里,所有感染奶牛一定形成若干段连续的 1。
例如:
001110011
可以拆成两段:
11111
由于 0 永远不会在最终时刻被感染,所以疾病不可能跨过这些 0。
因此每一段连续 1 都可以独立理解成:
- 若干个最初感染点
- 在同样的传播天数
T下 - 向左右扩散后形成的结果
第二步:先反推“最多传播了多少晚”
设传播了 T 晚。
对于一段长度为 len 的连续 1:
- 如果这段在中间,两边都是
0 - 那么最外侧那两个
1不可能再往外扩散到0
于是单个初始感染点最多只能在这一段里扩成长度 2T+1,所以必须满足:
2T + 1 <= len
也就是:
T <= (len - 1) / 2
如果这段贴着边界,比如在最左端或最右端,就只受一边 0 的限制,因此可以扩得更久,约束变成:
T <= len - 1
所以对所有连续 1 段,都能给出一个“这段允许的最大传播天数”。
全局真实传播天数 T 必须同时满足所有段的限制,因此应该取:
- 所有这些上界的最小值
这也是为了让初始感染数最少时,应该尽量取到的最大 T。
第三步:固定 T 后,每段最少需要多少个初始感染点
传播了 T 晚以后,一头最初感染的奶牛最多能覆盖一段长度:
2T + 1
所以对一段长度 len 的连续 1,最少需要:
ceil(len / (2T + 1))
头最初感染奶牛。
把所有连续段的这个值加起来,就是最终答案。
特殊情况:整串全是 1
如果最终整串都是 1,那么传播了多少晚都可以,只需要最开始有一头奶牛感染,最后总能扩满全串。
所以这种情况答案直接是 1。
代码
#include <bits/stdc++.h>
using namespace std;
int n;
string s;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
cin >> s;
s = " " + s;
// 把最终状态中的连续 1 全部提出来,后面按“每一段”分别分析。
vector<pair<int, int>> seg;
for (int i = 1; i <= n; ) {
if (s[i] == '0') {
i++;
continue;
}
int j = i;
while (j + 1 <= n && s[j + 1] == '1') {
j++;
}
seg.push_back(make_pair(i, j));
i = j + 1;
}
// 题面保证最终一定至少有一头奶牛感染。
// 这里额外做个保护,便于读者理解代码的完整性。
if (seg.empty()) {
cout << 0 << '\n';
return 0;
}
// 整串全是 1 时,可以只让一头奶牛最初感染。
if ((int)seg.size() == 1 && seg[0].first == 1 && seg[0].second == n) {
cout << 1 << '\n';
return 0;
}
int max_day = (int)1e9;
for (int i = 0; i < (int)seg.size(); i++) {
int l = seg[i].first;
int r = seg[i].second;
int len = r - l + 1;
// 反推这段能允许的最大传播天数。
// 贴边的连续段只受一侧 0 的限制;中间段受两侧限制。
if (l == 1 || r == n) {
max_day = min(max_day, len - 1);
} else {
max_day = min(max_day, (len - 1) / 2);
}
}
// 传播 max_day 晚以后,一头初始感染奶牛最终最多覆盖这么长的一段。
int cover = 2 * max_day + 1;
int ans = 0;
for (int i = 0; i < (int)seg.size(); i++) {
int len = seg[i].second - seg[i].first + 1;
// 当前连续段长度是 len,每个起点最多覆盖 cover,
// 所以这一段至少需要 ceil(len / cover) 个起点。
ans += (len + cover - 1) / cover;
}
cout << ans << '\n';
return 0;
}复杂度
- 时间复杂度:
- 空间复杂度:
(不算存输入串和分段信息时也可以视为 )
总结
这题的关键不是正着模拟传播,而是倒过来从最终状态反推:
- 连续
1段限制了传播天数T - 固定最大可行
T后,再计算每段至少需要多少个起点
一旦想清楚这两步,题目就是一个很干净的按段计数问题。
一图流解析
这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。
