[USACO23DEC] Cowntact Tracing 2 B

GitHub跳转原题关系图返回列表

先由最终连续感染段反推全局最多传播了多少晚,再把每段连续 1 按单个初始感染点最多覆盖的长度分组计数。

OJ: luogu

题目 ID: P9975

难度:普及+/提高

标签:思维字符串贪心

日期: 2026-06-20 11:26

同题版本

本题对应的 USACO 版本及解析:

思路

先看一个最直接的小数据暴力:

cpp
#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

可以拆成两段:

  • 111
  • 11

由于 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

代码

cpp
#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;
}

复杂度

  • 时间复杂度:O(n)O(n)
  • 空间复杂度:O(1)O(1)(不算存输入串和分段信息时也可以视为 O(n)O(n)

总结

这题的关键不是正着模拟传播,而是倒过来从最终状态反推:

  1. 连续 1 段限制了传播天数 T
  2. 固定最大可行 T 后,再计算每段至少需要多少个起点

一旦想清楚这两步,题目就是一个很干净的按段计数问题。

一图流解析

这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。

一图流解析