题目描述
原题来自:USACO 2003 Mar. Green
给定一个长度为 n 的非负整数序列 A ,求一个平均数最大的,长度不小于 L 的子段。
输入格式
第一行用空格分隔的两个整数 n 和 L;
第二行为 n 个用空格隔开的非负整数,表示 Ai。
输出格式
输出一个整数,表示这个平均数的 1000 倍。不用四舍五入,直接输出。
样例
输入数据 1
10 6
6 4 2 10 3 8 5 9 4 1
Copy
输出数据 1
6500
Copy
数据范围与提示
1≤n≤10^5,0≤Ai≤2000。
反思总结:
这道题是二分题
看到题目所给的数据和求字段的内容,我是怎么也不会想到用二分的方法来做这道题的。
按照正常的思路是先确定子段长度,再确定子段,然后再求平均值。这样的作法肯定是超时的。
这道题是使用二分的方法来缩小平均值得范围,这个方法的厉害之处在于,可以使用平均数值的大小确定子段的范围。
解题先知:如何判断平均数是大还是小:
令数列的每个数都减去现在的平均值,将的到的值相加,得到的和如果大于 0 就说明平均数小了,反之,则平均数大了,需要缩小一点。
1、将每一个数减去平均值的差保存下来。
2、将的到的差,通过前缀和优化。
3、从后面往前面枚举结尾的前缀和,通过在前面找最小的前缀和,然后相减就能得到当前子段差的前缀和。
4、判断差是否大于 0 。大于 0 就说明平均数小了,反之,则平均数大了,需要缩小一点。
需要注意:
一旦找到比 0 大的字段,马上就能进行平均值范围的变化了,不需要等到循环完才改变范围。
因为无论后面有没有比这个值大的子段,程序都会判断一次的,所以不需要非要找到最大的,只需要找到大的就可以了。
一般求子段可以使用前缀和来优化。这个精度的问题我也没注意到。
求子段和的时候,一边找最小前缀和,一边进行运算也是没有想到!
#include <stdio.h>
#include <math.h>
#include <algorithm>
#include <iostream>
#include <string.h>
#include <queue>
#include <stack>
#include <map>
#include <set>
#include <vector>
using namespace std;
const double dlt=1e-5;
int n,L;
double a[100007],b[100007],pre[100007];
int main() {
scanf("%d%d",&n,&L);
for(int i=1; i<=n; i++) {
scanf("%lf",&a[i]);
}
double l=-1e6,r=1e6;
while(r-l>dlt) {
int k=0;
double mi=(l+r)/2;
double mins=1e10;
for(int i=1; i<=n; i++) {
b[i]=a[i]-mi;
}
for(int i=1; i<=n; i++) {
pre[i]=pre[i-1]+b[i];
}
for(int i=L; i<=n; i++) {//一边找最小的前缀和,一边进行运算;这个也关键!
mins=min(pre[i-L],mins);
if(pre[i]-mins>=0) {
k=1;
l=mi;
break;
}
}
if(!k)
r=mi;
}
r=r*1000;
printf("%d\n",(int)r);
return 0;
}