题解:洛谷 P10450 Best Cow Fences

【题目来源】

洛谷:P10450 [USACO03MAR] Best Cow Fences G - 洛谷

【题目描述】

给定一个长度为 \(n\) 的非负整数序列 \(A\) ,求一个平均数最大的,长度不小于 \(L\) 的子段。

【输入】

第一行用空格分隔的两个整数 \(n\)\(L\)

第二行为 \(n\) 个用空格隔开的非负整数,表示 \(A_i\)

【输出】

输出一个整数,表示这个平均数的 \(1000\) 倍。不用四舍五入,直接输出。

【输入样例】

10 6
6 4 2 10 3 8 5 9 4 1

【输出样例】

6500

【核心思想】

  1. 问题分析:给定一个长度为 \(n\) 的非负整数序列 \(A\),求所有长度至少为 \(L\) 的连续子段中,平均数最大的那个,并输出该平均数的 \(1000\) 倍(直接截断取整)。这是一个典型的最大值平均值问题,直接枚举所有子段会超时(\(O(n^2)\)),需要利用二分答案 + 前缀和技巧将判定问题转化为是否存在满足条件的子段。

  2. 算法选择

    • 实数二分答案:对可能的平均值 \(x\) 进行二分。判定是否存在一个长度 \(\ge L\) 的子段,其平均值 \(\ge x\)
    • 前缀和优化判定:将原序列 \(A_i\) 减去 \(x\),得到新序列 \(B_i = A_i - x\)。那么一个子段 \([l, r]\) 的平均值 \(\ge x\) 等价于 \(\sum_{i=l}^{r} (A_i - x) \ge 0\),即前缀和之差 \(S_r - S_{l-1} \ge 0\)。因此只需检查是否存在 \(r - l + 1 \ge L\)\(S_r \ge S_{l-1}\)。扫描 \(r\) 时维护 \(S_{l-1}\) 的最小值即可 \(O(n)\) 判定。
  3. 关键步骤

    • 读入数据\(n, L\) 和数组 \(A[1..n]\)
    • 二分边界:平均数在 \([0, \max(A)]\) 之间,这里取 \([0, 2000]\)(因 \(A_i \le 2000\))。
    • 判定函数 check(x)
      1. 计算前缀和数组 sum[0..n],其中 sum[i] = sum[i-1] + A[i] - x
      2. 初始化 minn = 0(表示 \(S_0\))。
      3. 遍历 \(i\)\(L\)\(n\)
        • 更新 minn = min(minn, sum[i-L])(维护 \(S_0\)\(S_{i-L}\) 的最小值)。
        • sum[i] - minn >= 0,则存在长度 \(\ge L\) 的子段平均值 \(\ge x\),返回 true
      4. 若遍历结束未找到,返回 false
    • 更新二分区间:若 check(mid) 为真,则 l = mid(尝试更大平均值);否则 r = mid
    • 输出(int)(r * 1000)(直接截断,不四舍五入)。
  4. 时间/空间复杂度

    • 时间复杂度:\(O(n \log C)\),其中 \(C\) 是二分范围与精度要求的比值(通常约 \(60\) 次迭代),\(n \le 10^5\),完全可行。
    • 空间复杂度:\(O(n)\),用于存储前缀和数组。
  5. 二分答案 + 前缀和判定的核心思想

    • 问题转化:将“求最大平均值”转化为“判定是否存在平均值 \(\ge x\)”,利用单调性进行二分。
    • 平均值判定技巧:通过减去 \(x\),将平均值条件转化为区间和 \(\ge 0\) 的条件,从而用前缀和在 \(O(n)\) 内判定。
    • 最小前缀和维护:在扫描右端点 \(r\) 时,只需要知道之前最小的前缀和即可判断是否存在以 \(r\) 结尾且长度 \(\ge L\) 的合法子段。
    • 精度控制:实数二分 \(1e-5\) 精度足以满足输出整数倍的精度要求(乘以 \(1000\) 后误差小于 \(0.01\))。
    • 适用场景:适用于求解“最大平均值子段”或“最小平均值子段”问题,核心是二分答案 + 前缀和优化判定。

【算法标签】

普及 #浮点二分

【代码详解】

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

const int N = 100005;  // 定义最大数组长度

int n;                  // 数组长度(元素个数)
int L;                  // 最小子数组长度
int a[N];               // 存储原始数据的数组
double sum[N];          // 前缀和数组(存储a[i]-x的累加和)

/**
 * 检查是否存在长度至少为L的子数组,其平均值大于等于x
 * @param x 要检查的平均值阈值
 * @return 存在满足条件的子数组返回true,否则false
 */
bool check(double x)
{
    // 计算前缀和数组:sum[i] = (a[1]-x) + (a[2]-x) + ... + (a[i]-x)
    for (int i = 1; i <= n; i++)
    {
        sum[i] = sum[i - 1] + a[i] - x;
    }

    double minn = 0;  // 存储前i-L个位置的最小前缀和

    // 检查所有长度至少为L的子数组
    for (int i = L; i <= n; i++)
    {
        // 更新前i-L个位置的最小前缀和
        minn = min(minn, sum[i - L]);

        // 如果当前前缀和减去最小前缀和≥0,说明存在满足条件的子数组
        if (sum[i] - minn >= 0)
        {
            return true;
        }
    }

    return false;
}

int main()
{
    // 输入数组长度和最小子数组长度
    cin >> n >> L;

    // 输入数组元素
    for (int i = 1; i <= n; i++)
    {
        cin >> a[i];
    }

    // 二分查找的左右边界
    double l = 0, r = 2000, mid;

    // 二分查找最大平均值,精度控制到1e-5
    while (r - l > 1e-5)
    {
        mid = (l + r) / 2;

        // 检查是否存在平均值≥mid的子数组
        if (check(mid))
        {
            l = mid;  // 平均值可以更大,向右搜索
        }
        else
        {
            r = mid;  // 平均值过大,向左搜索
        }
    }

    // 输出结果(乘以1000后取整)
    cout << (int)(r * 1000);

    return 0;
}

【运行结果】

10 6
6 4 2 10 3 8 5 9 4 1
6500
posted @ 2026-08-29 09:36  团爸讲算法  阅读(11)  评论(0)    收藏  举报