最佳牛围栏(二分答案转化为判定)
题目来源:https://www.acwing.com/problem/content/104/
算法标签:二分,前缀和,双指针
题目内容:
农夫约翰的农场由 N块田地组成,每块地里都有一定数量的牛,其数量不会少于1头,也不会超过2000头。约翰希望用围栏将一部分连续的田地围起来,并使得围起来的区域内每块地包含的牛的数量的平均值达到最大。围起区域内至少需要包含 F 块地,其中 F 会在输入中给出。在给定条件下,计算围起区域内每块地包含的牛的数量的平均值可能的最大值是多少。
输入格式:
第一行输入整数 N 和 F ,数据间用空格隔开。接下来 N 行,每行输入一个整数,第i+1行输入的整数代表第i片区域内包含的牛的数目。
输出格式:
输出一个整数,表示平均值的最大值乘以1000再 向下取整 之后得到的结果。
数据范围:
1≤N≤100000
1≤F≤N
输入样例:
10
6
6
4
2
10
3
8
5
9
4
1
输出样例:
6500
个人思考:
由于本人比较菜,当时想去暴力求解,也就是从头到尾算连续F个田地的牛的平均值并且用数组存起来然后再求出数组中的最大值,但是后来看到连续的田地数量是大于等于F(并不是一定等于F),并且数据范围1≤N≤100000 ,1≤F≤N(暴力做肯定超时),然后就放弃了。。。
问题抽象:
题目可以抽象地理解为给定正整数序列A,求出一个平均数最大的,长度不小于L的(连续的)子段(但是输出的是:该子段的平均值(保留三位有效数字)并且乘1000的结果)
正确的思考方向:
本题所要求的平均值一定在[1,2000]的闭区间上(从题目中可以得出),不妨换一种思路,将求解过程转换为在该闭区间上二分判定答案的过程,也就是在该区间上不断地进行二分,每找到一个mid就判断一下该mid是否能够由当前输入所给的数据得到,如果当前mid可以得到,那么比该mid小的数也一定可以在当前题目所给的数据下得到,然后就只需要在mid及其右半边的数上进行二分判断,直到二分区间的长度小于等于题目精度此时的r也就是答案(至于为啥是r,而不是l,其实可以自己试一试,问题不大)。经过这样的转换以后,问题就变成了给你一个平均值,让你判断输入数据是否存在一个可行的子段和的平均值能够大于等于它。
具体操作思路:
虽然说如果能想到将问题转换为二分判定答案以后问题就已经解决了一大半,但是这后面还是有很多细节,如果没想到一样是无法AC。
1.初始二分区间可以设为[1,2000],并求出mid
2.将输入的数据全部减去mid(至于为啥减去mid,如果说想找到一个子段和的平均值大于等于mid,也可以转化为将该子段的数据全部减去mid并求和判断和是否大于等于0,主要是为了方便后续操作,如果不进行该操作,后续操作应该也是可以做的)
3.求前缀和(每当涉及到求连续子段和时,都应该想到用前缀和去处理,例如求[l,r]子段和,求出前缀和后只需要用S[r]-S[l-1]即可得到[l,r]的子段和)
新的问题:
本来需要判断S[r]-S[l-1]>=0,即判断S[r]>=S[l-1](r-l>=F),又由于该子段和的长度可以大于F,那么如果S[r]<S[l-1],则还需判断S[r]>=S[l-2],更糟的话还需要将S[r]与S[1]…S[l-1]进行比较,此时时间复杂度会很高,运行代码会超时。此时可以思考只要S[1]到S[l-1]中存在一个小于等于S[r]那么该mid就可以被满足,所以我们只需要用一个数保存S[1]到S[l-1]中的最小值即可。
源代码:
#include<iostream>
#define eps 1e-5//此处是二分精度,题目所给保留精度为1e-3,实数域上的二分精度一般在题目所给精度上乘1e-2
using namespace std;
const int N=100010;
double a[N],b[N];
int check(double mid,int length,int F)//因为主函数中定义的double mid传参定义的int mid害我debug1个多小时
{
for(int i=1;i<=length;i++)
b[i]=a[i]-mid+b[i-1];//b为前缀和数组,此处在减去mid的同时求出前缀和
double minnum=b[0];
int i=0;
int j=i+F;
for(;j<=length;j++)
{
if(b[j]-minnum>=0)
return 1;//如果有存在前缀和>=0那么直接退出函数
minnum=min(minnum,b[++i]);//每一轮循环都要对minnum进行更新,minnum是S[0]到S[i-1]中最小的数
}
return 0;
}
int main()
{
int N,F;
cin>>N>>F;
for(int i=1;i<=N;i++)
cin>>a[i];
double l=1,r=2000;
while(l+eps<r)//实数域的二分模板
{
double mid=(l+r)/2;
if(check(mid,N,F))
l=mid;
else
r=mid;
}
cout<<(int)(r*1000)<<endl;
return 0;
}