题解:洛谷 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
【核心思想】
-
问题分析:给定一个长度为 \(n\) 的非负整数序列 \(A\),求所有长度至少为 \(L\) 的连续子段中,平均数最大的那个,并输出该平均数的 \(1000\) 倍(直接截断取整)。这是一个典型的最大值平均值问题,直接枚举所有子段会超时(\(O(n^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)\) 判定。
-
关键步骤:
- 读入数据:\(n, L\) 和数组 \(A[1..n]\)。
- 二分边界:平均数在 \([0, \max(A)]\) 之间,这里取 \([0, 2000]\)(因 \(A_i \le 2000\))。
- 判定函数
check(x):- 计算前缀和数组
sum[0..n],其中sum[i] = sum[i-1] + A[i] - x。 - 初始化
minn = 0(表示 \(S_0\))。 - 遍历 \(i\) 从 \(L\) 到 \(n\):
- 更新
minn = min(minn, sum[i-L])(维护 \(S_0\) 到 \(S_{i-L}\) 的最小值)。 - 若
sum[i] - minn >= 0,则存在长度 \(\ge L\) 的子段平均值 \(\ge x\),返回true。
- 更新
- 若遍历结束未找到,返回
false。
- 计算前缀和数组
- 更新二分区间:若
check(mid)为真,则l = mid(尝试更大平均值);否则r = mid。 - 输出:
(int)(r * 1000)(直接截断,不四舍五入)。
-
时间/空间复杂度:
- 时间复杂度:\(O(n \log C)\),其中 \(C\) 是二分范围与精度要求的比值(通常约 \(60\) 次迭代),\(n \le 10^5\),完全可行。
- 空间复杂度:\(O(n)\),用于存储前缀和数组。
-
二分答案 + 前缀和判定的核心思想:
- 问题转化:将“求最大平均值”转化为“判定是否存在平均值 \(\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
浙公网安备 33010602011771号