题解:洛谷 P10133 Balancing Bacteria

【题目来源】

洛谷:P10133 [USACO24JAN] Balancing Bacteria B - 洛谷

【题目描述】

Farmer John 有 \(N\)\(1\le N\le 2\cdot 10^5\))块草地排成一行,其中草地 \(i\) 的细菌水平与健康草的细菌水平相差 \(a_i\)\(−10^{15}\le a_i\le 10^{15}\))。例如,如果 \(a_i=−3\),则草地 \(i\) 的细菌水平比正常水平低 \(3\),需要额外添加恰好 \(3\) 个单位的细菌才能将其提高到被认为是健康的程度。

Farmer John 想要确保每一块草地都被修复至健康的细菌水平。方便的是,他有两种品牌的农药可以喷洒在他的田地里,一种可以添加细菌,另一种可以去除细菌。当 Farmer John 喷洒任一类型的农药时,他站在草地 \(N\)(最右边的草地)并为他的喷雾器选择功率等级 \(L\)\(1\le L\le N\))。

喷雾器对靠近 Farmer John 的草地效果最大,随着距离增加效果逐渐减弱。如果 Farmer John 选择添加细菌的农药,则 \(L\) 单位的细菌将被添加至草地 \(N\)\(L−1\) 单位添加至草地 \(N−1\)\(L−2\) 单位添加至草地 \(N−2\),以此类推。草地 \(1\ldots N−L\) 不会得到任何细菌,因为喷雾器设置的功率不足以到达它们。类似地,如果 Farmer John 选择去除细菌的农药,则 \(L\) 单位的细菌将被从草地 \(N\) 去除,\(L−1\) 单位被从草地 \(N−1\) 去除,以此类推。同样,草地 \(1\ldots N−L\) 将不受影响。

求 Farmer John 使用喷雾器的最少次数,使得每块草地都具有健康草的推荐细菌值。输入保证答案不超过 \(10^9\)

注意这个问题涉及到的整数可能需要使用 64 位整数型(例如,C/C++ 中的 "long long")。

【输入】

输入的第一行包含 \(N\)

第二行包含 \(N\) 个整数 \(a_1\ldots a_N\),为每块草地的初始细菌水平。

【输出】

输出一个整数,为使每块草地都具有健康草的推荐的细菌值所需使用喷雾器的最少次数。

【输入样例】

2
-1 3

【输出样例】

6

【核心思想】

  1. 问题分析:有 \(N\) 块草地,每块草地 \(i\) 的当前细菌水平为 \(a_i\)(可为负),目标是将其全部变为 \(0\)。每次操作:站在草地 \(N\)(最右端),选择功率 \(L\)\(1 \le L \le N\))和类型(添加或去除),操作效果为:对于 \(j = 0, 1, \dots, L-1\),草地 \(N-j\) 的细菌水平变化为 \(\pm (L-j)\)(即从右向左递减 1)。求将所有 \(a_i\) 归零的最少操作次数。本质:每次操作相当于在差分数组的最右端施加一个形如 \([L, L-1, \dots, 1]\) 的线性递减向量。由于操作只影响从某个位置到右端的一段,且幅度线性递减,考虑使用差分将区间操作转化为单点修改。

  2. 算法选择

    • 差分 + 贪心扫描:由于操作是从右向左的递减序列,从右向左处理 \(a_i\)。维护当前已经施加的“累计操作效果” \(b_i\)\(c_i\)(分别代表添加和去除操作对当前位置的影响)。每步计算需要的调整量 \(\Delta\),通过增加某种操作类型来补偿,并更新累计效果。最终操作次数等于所有调整量的绝对值之和再加 1(因为每次操作至少改变最右端,最后还需要一次操作固定末尾)。更标准的解法:将 \(a\) 转化为差分数组,每次操作等价于在差分数组的两个位置做修改,问题转化为用最少操作使差分数组归零。
  3. 关键步骤(基于代码逻辑):

    • 跳过前导零:从第一个非零位置 mark 开始处理(前面的零不需要操作)。
    • 确定目标模式:若第一个非零值为负,则希望从 mark\(N\) 形成递减等差数列(每次 -1);若为正,则形成递增等差数列(每次 +1)。因为操作从右向左递减,要消除一个负数段,需要从左侧开始逐步增加差值。
    • 扫描调整:从 mark\(N\),维护两个累积量 b(添加操作累积效果)和 c(去除操作累积效果)。计算当前位置实际值与目标值(等差数列)的偏差 \(\Delta\),根据 \(\Delta\) 的正负调整 b_gcc_gc(全局增量),并累加 \(|\Delta|\) 到答案。
    • 最终修正:处理完所有位置后,检查最后一个位置是否达到目标等差数列的终点,若否,则补上差值。
    • 输出 ans + 1
  4. 时间/空间复杂度

    • 时间复杂度:\(O(N)\)
    • 空间复杂度:\(O(N)\),存储数组。
  5. 差分 + 贪心构造的核心思想

    • 操作分解:一次功率为 \(L\) 的操作可以看作在差分数组上:在位置 \(N-L+1\) 处加上(或减去)\(L\),在位置 \(N+1\)(虚拟边界)处减去(或加上)\(L\),以及中间每个位置差分值变化 1。代码采用直接构造等差数列的方式,将问题转化为:用最少的操作次数,使得原数组的“二阶差分”归零。
    • 贪心调整:从第一个非零位置开始,逐个将 \(a_i\) 调整为与前一项相差 1(正负取决于符号),每一步的调整量 \(\Delta\) 直接对应需要额外施加的操作次数。这是因为每次操作对位置 \(i\)\(i-1\) 的影响差值固定为 1(从右向左递减),因此要改变相邻两项的差值,只需增加相应次数的操作。
    • 适用场景:适用于操作效果为线性递减区间、且需要将数组归零的优化问题。核心是利用差分将区间操作转化为相邻差值的调整,从而贪心计数。

【算法标签】

普及 #差分

【代码详解】

#include <bits/stdc++.h>
using namespace std;

int n;
long long a[200005], b[200005], c[200005], ans, delta;
long long b_gc, b_st, c_gc, c_st;

int main()
{
    cin >> n;
    int cnt = 0;

    // 读取数组a并统计0的个数
    for (int i = 1; i <= n; i++)
    {
        cin >> a[i];
        if (a[i] == 0)
        {
            cnt++;
        }
    }

    // 如果数组全为0,直接输出0
    if (cnt == n)
    {
        cout << 0 << endl;
        return 0;
    }

    // 找到第一个非0元素的位置
    int mark = 1;
    for (int i = 1; i <= n; i++)
    {
        if (a[i] == 0)
        {
            continue;
        }
        else
        {
            mark = i;
            break;
        }
    }

    // 根据第一个非0元素的正负性分别处理
    if (a[mark] < 0)
    {
        // 处理负数序列
        for (int i = mark; i <= n; i++)
        {
            // 计算b和c数组的当前值(基于增量)
            b[i] = b[i - 1] + b_gc;
            c[i] = c[i - 1] + c_gc;

            // 计算目标值与当前值的差值
            // 目标:形成递减等差数列 a[i] = a[i-1] - 1
            delta = (a[i - 1] - 1) - (a[i] + b[i] + c[i]);

            // 根据差值调整增量
            if (delta > 0)
            {
                b_gc = b_gc + delta;  // 正偏差,增加b的增量
            }
            else if (delta < 0)
            {
                c_gc = c_gc + delta;  // 负偏差,减少c的增量
            }

            // 更新b和c的累积值
            b_st = b_st + b_gc;
            b[i] = b_st;
            c_st = c_st + c_gc;
            c[i] = c_st;

            // 更新a[i]为期望的等差数列值
            a[i] = a[i - 1] - 1;

            // 累加调整代价
            ans += abs(delta);
        }

        // 检查最后一个元素是否符合等差数列
        if (a[n] != a[n - 1] - 1)
        {
            ans += abs(a[n - 1] - 1 - a[n]);
        }
    }
    else if (a[mark] > 0)
    {
        // 处理正数序列(逻辑与负数情况对称)
        for (int i = mark; i <= n; i++)
        {
            b[i] = b[i - 1] + b_gc;
            c[i] = c[i - 1] + c_gc;

            // 目标:形成递增等差数列 a[i] = a[i-1] + 1
            delta = (a[i - 1] + 1) - (a[i] + b[i] + c[i]);

            if (delta > 0)
            {
                b_gc = b_gc + delta;
            }
            else if (delta < 0)
            {
                c_gc = c_gc + delta;
            }

            b_st = b_st + b_gc;
            b[i] = b_st;
            c_st = c_st + c_gc;
            c[i] = c_st;

            a[i] = a[i - 1] + 1;
            ans += abs(delta);
        }

        // 检查最后一个元素
        if (a[n] != a[n - 1] + 1)
        {
            ans += abs(a[n - 1] + 1 - a[n]);
        }
    }

    // 输出总调整代价+1
    cout << ans + 1 << endl;
    return 0;
}

【运行结果】

2
-1 3
6
posted @ 2026-08-28 15:53  团爸讲算法  阅读(1)  评论(0)    收藏  举报