题解:洛谷 P9975 Cowntact Tracing 2

【题目来源】

洛谷:P9975 [USACO23DEC] Cowntact Tracing 2 B - 洛谷

【题目描述】

Farmer John 有 \(N\) 头奶牛排成一列(\(1 \leq N \leq 3\cdot 10^5\))。不幸的是,有一种疾病正在传播。

最初,有一些奶牛被感染。每到夜晚,被感染的奶牛会将疾病传播给它左右两边的奶牛(如果这些奶牛存在的话)。一旦奶牛被感染,她就会持续处于感染状态。

经过一些晚上,Farmer John 意识到情况已经失控,因此他对奶牛进行了检测以确定哪些奶牛感染了疾病。请找出最少有多少头奶牛最初可能感染了这种疾病。

【输入】

第一行为一个整数 \(N\),即 Farmer John 拥有的奶牛数量。

接下来一行,包含长度为 \(N\) 的由 \(1\)\(0\) 组成的位串。其中 \(1\) 表示一头被感染的奶牛,\(0\) 表示一头在经过若干晚之后仍未被感染的奶牛。

【输出】

输出一个整数,表示最少有多少头奶牛可能最初感染了这种疾病。

【输入样例】

5
11111

【输出样例】

1

【核心思想】

  1. 问题分析:给定一个长度为 \(N\) 的 0/1 字符串,1 表示最终被感染的奶牛,0 表示始终未被感染。传播规则:每晚,已感染的奶牛会将其左右相邻的健康奶牛感染。求最少可能的初始感染奶牛数量。核心在于:连续 1 段的长度决定了该段所需的最少初始感染数,而连续 0 段的长度(包括两端)限制了传播的“时间窗口”——即最多能传播多少晚,从而决定了每个 1 段内部可以合并的初始感染点数量。

  2. 算法选择

    • 贪心 + 分段处理:将字符串分为左端连续 1 段中间若干由 0 分隔的 1 段右端连续 1 段。每段所需最小初始感染数由段长和全局“传播能力”(即最大的连续 0 段长度决定的最多传播天数)共同决定。
    • 关键变量minn 表示所有连续 1 段对应的最大传播天数的最小值。传播天数不能超过任何一段的限制,因此取所有限制的最小值作为全局可用传播天数。
  3. 关键步骤(基于代码逻辑):

    • 特判全 1 或全 0:全 1 只需 1 头初始感染;全 0 则为 0。
    • 特判模式:如 "010""10""01""0110" 等,这些情况初始感染数等于当前 1 的个数(传播受限,无法合并)。
    • 计算左端连续 1 长度 left1 和右端连续 1 长度 right1
    • 计算中间连续 1 段的长度数组 a[group](被 0 分隔)。
    • 确定最小传播天数 minn
      • 左端 left1 能传播的最大天数为 left1 - 1
      • 右端同理。
      • 中间长度为 len 的 1 段,最大传播天数为 \(\lfloor (len-1)/2 \rfloor\)(奇数 (len-1)/2,偶数 (len-2)/2)。
      • minn 取上述所有值的最小值。
    • 计算总初始感染数 sum
      • 对每个中间 1 段,所需初始感染数为 \(\lceil len / (2 \cdot minn + 1) \rceil\)
      • 对左端 1 段,若 left1 <= minn 则只需 1 头;否则需要 \(\lceil (left1 - minn) / (2 \cdot minn + 1) \rceil + 1\) 等。
      • 右端同理。
    • 输出 sum
  4. 时间/空间复杂度

    • 时间复杂度:\(O(N)\),线性扫描。
    • 空间复杂度:\(O(N)\),存储数组和字符串。
  5. 贪心 + 分段覆盖的核心思想

    • 传播等价于区间扩张:一个初始感染点经过 \(t\) 晚后,会覆盖其左右各 \(t\) 个位置,形成一个长度为 \(2t+1\) 的连续 1 段。因此,给定一个最终 1 段,求最少初始点等价于用若干长度为 \(2t+1\) 的区间覆盖它。
    • 传播天数受限\(t\) 不能超过该 1 段两侧 0 段的长度(因为传播到 0 段边界就会停止,无法跨越)。因此,全局可用的最大传播天数 \(t\) 是所有 1 段对应限制的最小值。
    • 贪心覆盖:确定 \(t\) 后,每个 1 段需要的最少初始点数就是 \(\lceil len / (2t+1) \rceil\)(中间段),边界段由于只需向一侧传播,覆盖长度为 \(t+1\),故公式略有不同。
    • 适用场景:适用于一维传播模型,已知最终感染状态,求最小初始源点数量。核心是识别连续段并利用传播对称性。

【解题思路】

img/Pasted image 20260519130101.png

【算法标签】

普及 #贪心

【代码详解】

#include <bits/stdc++.h>
using namespace std;
int n, minl=1e9, maxl=-1e9, minr=1e9, maxr=-1e9, minn=1e9, minm=1e9, left1=0, right1=0, sum=0;
int cnt0=0, cnt1=0;
int a[300005];
string s;
int main()
{
    cin >> n >> s;  // 输入n和0、1字符串
    s = " " + s + " ";  // 字符串的2端增加空格
    for (int i=1; i<=n; i++) {  // 统计1的个数
        if (s[i]=='1') cnt1++;
        else cnt0++;
    }
    if (cnt1==n) {  // 如果全为1,则输出1
        cout << 1 << endl;
        exit(0);
    }
    if (cnt0==n) {  // 如果全为0,则输出0
        cout << 0 << endl;
        exit(0);
    }
    if ((s.find("010")>=0 && s.find("010")<=n) || (s.find(" 10")>=0 && s.find(" 10")<=n) || (s.find("01 ")>=0 && s.find("01 ")<=n) || (s.find("0110")>=0 && s.find("0110")<=n)) {  // 进行特判,该些场景下最初1的个数就是现在1的个数
        cout << cnt1 << endl;
        exit(0);
    }
    int st=1, ed=n;  // 定义起始和结束位置
    if (s[1]=='1') {  // 从起始位置开始,找左端连续1的个数
        for (int i=1; i<=n; i++) {
            if (s[i]=='0') {
                st = i;
                break;
            }
            left1++;
        }
    }
    if (left1>0) {  // 如果有的话,left1可调整的最小值为left1-1(其实就是最大值)
        minn = min(minn, left1-1);
    }
    if (s[n]=='1') {  // 从结束位置开始,找右端连续1的个数
        for (int i=n; i>=1; i--) {
            if (s[i]=='0') {
                ed = i;
                break;
            }
            right1++;
        }
    }
    if (right1>0) {  // 如果有的话,right1可调整的最小值为right1-1(其实就是最大值)
        minn = min(minn, right1-1);
    }
    int tmp = 0, group=0;  // 定义中间连续1个个数tmp和有多少组group
    char last = s[st];  // 此时st位置为从左边的第一个0
    for (int i=st+1; i<=ed; i++) {  // 从左边的第一个0开始
        if (s[i]=='1') {  // 如果是1
            tmp++;  // 个数自增1
        } else if (last=='1' && s[i]!='1'){  // 如果遇到1之后的第一个0
            a[group] = tmp;  // 记录该连续1的个数,并存到数组中
            tmp=0;  // 重置tmp为0
            group++;  // 组数自增1
        }
        last = s[i];  // 更新last为当前字符
    }
    for (int i=0; i<group; i++) {  // 统计中间连续1中最小调整次数
        if (a[i]%2==1) {  // 分奇偶讨论
            minn = min(minn, (a[i]-1)/2);
        } else {
            minn = min(minn, (a[i]-2)/2);
        }
    }
    for (int i=0; i<group; i++) {  // 得到最小调整次数后,计算中间每段连续1的初始1的个数,并累积求和
        sum += (a[i]+(2*minn+1-1))/(2*minn+1);
    }
    if (left1>0) {  // 计算左端连续1的初始1个数
        int tmp = (left1+2*minn)/(2*minn+1);
        if (tmp>=left1) {  // 当根据公式计算的值大于等于left1个数时
            sum += left1-minn;  // 就加上left1-minn,即开始最左边为1
        } else {  // 否则像计算中间一样计算并累加
            sum += tmp;
        }
    }
    if (right1>0) {  // 计算右端连续1的初始1个数
        int tmp = (right1+2*minn)/(2*minn+1);
        if (tmp>=right1) {  // 当根据公式计算的值大于等于right1个数时
            sum += right1-minn;  // 就加上right1-minn,即开始最右边为1
        } else {  // 否则像计算中间一样计算并累加
            sum += tmp;
        }
    }
    cout << sum << endl;  // 输出结果
    return 0;
}

【运行结果】

5
11111
1
posted @ 2026-08-30 09:18  团爸讲算法  阅读(9)  评论(0)    收藏  举报