一道经典的二分答案转判断问题,稍微特殊的点就是这道题是实数域上的二分,看上去比较唬人,但实际上比整数集合上的二分好写不少。
## 题目分析与思路:
给定一个数组A,我们其中要求一个平均数最大的,且长度不小于给定值L的一个子段,那么我们就可以去**用二分枚举这个最大的平均数**,这不难理解。但这道题的难点在于,如何去写check函数,也就是如何去判断我们枚举的这个数。
我们对check函数进行如下思考:
1.如果我们能在A数组中找到比我们枚举的这个平均数更大的平均数且这个平均数所在的子段大于等于L,那么我们就可以缩短左区间,反之则缩右区间。上述思路也就是我们要在check函数内实现的效果。
2.若是按普通的思路,双指针遍历所有A数组内长度大于等于L的区间,复杂度为O($x^2$),显然会超时。对于100000大小的数据,最多只能用O(nlogn)复杂度的算法进行解决。也就是说**在check函数中我们需要以O(n)的时间复杂度完成查找**。
3.这里我们的思路是:将A[i]-avg(我们传入check函数中的,也就是我们所枚举的那个平均值)作为一个新的数组,然后对这个数组求前缀和。说明一下:
假设我们要求某一段区间的平均值,我们应该是把这一段的所有值加起来在得到sum再除以区间内数的个数n,也就是:
稍微改变一下形式就好理解了:
得到了如上表达式,我们就只需要把avg带进去,计算它与0的大小关系就能得到该区间的平均值与avg的大小关系。这样的一个形式,我们很容易想到前缀和。
我们设前缀和数组s,i=0,j=L,每次使两个数++。因为i,j始终满足相距L的距离,所以我们用一个变量minx来存储s[i]所遍历到的最小值,这样我们比较的距离一定是≥L的,此时若用s[j]去减去minx的话,就能得到我们的最优解,如果这个最优解>= 0 那么就满足我们的指定条件
check函数代码如下:
inline bool check(double avg)
{
double s[100005] = { 0 };//前缀和数组
for (int i = 1;i <= n;i++)
s[i] = s[i - 1] + A[i] - avg;
double minx = 0;
for (int i = 0, j = L;j <= n;i++, j++)//注意这里i要从0开始遍历
{
minx = min(s[i], minx);//寻找最优极小值
if (s[j] - minx >= 0)//判断
return true;
}
return false;
}
## 完整代码:
#include<bits/stdc++.h>
using namespace std;
#define int long long
int A[100005];
double n, L;
inline bool check(double avg)
{
double s[100005] = { 0 };
for (int i = 1;i <= n;i++)
s[i] = s[i - 1] + A[i] - avg;
double minx = 0;
for (int i = 0, j = L;j <= n;i++, j++)//注意这里i要从0开始遍历
{
minx = min(s[i], minx);//寻找最优极小值
if (s[j] - minx >= 0)//判断
return true;
}
return false;
}
signed main()
{
cin >> n >> L;
for (int i = 1;i <= n;i++)
cin >> A[i];
//设立左右区间
double l, r;l = r = 0;
for (int i = 1;i <= n;i++)
r = max(r, (double)A[i]);
//二分循环
while (r - l > 1e-5)
{
double mid = (l + r) / 2;//因为是实数域上的二分,不可以直接使用 >>1
if (check(mid))
l = mid;
else r = mid;
}
cout << (int)(r * 1000);
return 0;
}
最后还有一点需要说明的是:当我们用实数二分求一个值的时候,这个值会趋近于答案,随着精度的提高,这两边会越来越趋近于答案,r比答案大一点,l 比答案小一点,所以我们必须用r向下取整来得到答案。
同理,如果题目要求最终答案向上取整,就必须用l向上取整来得到最终答案。