最佳牛围栏
题目链接
题目描述:
农夫约翰的农场由 N块田地组成,每块地里都有一定数量的牛,其数量不会少于 11 头,也不会超过 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
分析:
原问题是找到一个长度大于L的连续的区域,使得该区域内每块地包含的牛的数量的平均值最大。如果直接求显然需要O(n2)的复杂度。而由题目易知这个最大平均值范围是1~2000,所以可以考虑二分,使用二分搜索来找到可能的最大平均值x。每次二分出一个可能的 x 值,看看是否存在一个长度大于L的区间,其平均牛数不小于这个 x。这就是二分的check()函数需要判断的问题。利用二分将直接找最大平均值,转换为判断是否存在一段长度大于L的连续区域的平均值大于等于x。
check()函数的实现
- 判断是否存在一段长度大于L的连续区域的平均值大于等于x。对于一个长度为 L 的区域,其总牛数应满足: 总牛数≥L*x。
- 如何快速判断某个区间总牛数是否大于L*x呢?显然需要使用前缀和来快速计算区间总牛数。但是即使是使用前缀和优化,也仍然需要O(n2)的复杂度来枚举所有可能区间来判断是否存在平均值大于x的区间。
- 我们可以在不等号两边同时减去L*x,即让每块地都减去平均值x。从而只需要判断总牛数是否大于0。通过减去 x 并计算前缀和转换问题:现在,我们只需检查是否存在一个区间,使得该区间的前缀和非负。
- 如何快速找到长度大于L的前缀和非负区间呢,已知区间和为s[l]s[r],可以枚举区间右端点,对于每一个区间右端点r,其左端点l可以是0r-L+1,显然s[l]越小越好,所以需要确定s[0]-s[r-L+1]哪个最小。我们可以计算出一个st[]数组用于存储s[0][i]中值最小的下标,例如st[4]=2表示s[0]s[4]中s[2]最小,通过st数组可以快速找到使区间和最大的左端点。如果这个区间和都非负,那么以r为右端点的所有区间都将非负。
实现代码:
#include<cstdio>
#include<iostream>
#include<iomanip>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<string>
#include<queue>
#include<stack>
#include<map>
#include<unordered_map>
#include<set>
#include<unordered_set>
#include<vector>
#include<bitset>
#include<deque>
#include<cctype>
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int n,f,st[N];//用st数组存前缀和数组s[N]前i个数的最小元素的下标
double a[N],b[N];
//st[8]=2表示s[1]~s[8]最小的数是s[2];
bool check(double x)
{
memcpy(b,a,sizeof a);
for(int i=1;i<=n;i++)
{
a[i]-=x;//每个元素都减去x
a[i]=a[i-1]+a[i];//构造前缀和
}
//构造st数组
st[1]=1;
for(int i=2;i<=n;i++)
{
if(a[i]<a[st[i-1]]) st[i]=i;
else st[i]=st[i-1];
}
//只要存在某段长度大于f的区间元素和大于0,则return true
for(int j=f;j<=n;j++)
{
if(a[j]-a[st[j-f]]>=0) return true;
}
return false;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin>>n>>f;
for(int i=1;i<=n;i++)
{
cin>>a[i];
}
//答案一定是1~2000,对这个答案进行二分
double l=1,r=2000;
while(r-l>1e-5)
{
double mid=(l+r)/2;
if(check(mid)) l=mid;
else r=mid;
memcpy(a,b,sizeof a);
}
int res=r*1000;//只能使用r
cout<<res;
}
注意:
最终的输出一定要用r来获取结果而不能用l。虽然按照check(mid)函数的逻辑,它在mid是一个可行的解或者存在比mid更大的解时才返回true。这意味着循环结束时,l是一个等于或者小于正确答案的解,而r是一个大于正确答案的解。由于二分精度限制,最终可能存在最终l小于正确答案而r大于最终答案,例如l=6.499999而r=6.500001而正确答案是6.5。由于需要向下取值,如果使用l获取答案会输出错误结果。所以这里必须使用r来获取答案,其本质原因是题目向下取值 的要求。