深刻理解二分
最佳牛围栏
农夫约翰的农场由 $ N $ 块田地组成,每块地里都有一定数量的牛,其数量不会少于 $ 1 $ 头,也不会超过 $ 2000 $ 头。
约翰希望用围栏将一部分连续的田地围起来,并使得围起来的区域内每块地包含的牛的数量的平均值达到最大。
围起区域内至少需要包含 $ F $ 块地,其中 $ F $ 会在输入中给出。
在给定条件下,计算围起区域内每块地包含的牛的数量的平均值可能的最大值是多少。
输入格式
第一行输入整数 $ N $ 和 $ F $,数据间用空格隔开。
接下来 $ N $ 行,每行输入一个整数,第 $ i+1 $ 行输入的整数代表第 $ i $ 片区域内包含的牛的数目。
输出格式
输出一个整数,表示平均值的最大值乘以 $ 1000 $ 再 向下取整 之后得到的结果。
数据范围
$ 1 \le N \le 100000 $
$ 1 \le F \le N $
输入样例:
10 6
6
4
2
10
3
8
5
9
4
1
输出样例:
6500
一般的二分标志词:最大值最小、最小值最大等
但二分的根本使用条件是:二段性,即只要满足可以找到一个值一半满足一半不满足即可
针对本题而言,我们二分的就是答案(二分的是实数类型的平均值average)。这里想说一下,一般二分就直接二分我们直接求取的答案,而不是枚举一些中间量去推出答案。比如我开始的时候想二分的是最后取得最大平均值时的围栏数f(\(1 \le f \le N\)).那么check函数中不仅要把所有可能连续的$ \ge f$ 的情况枚举一遍,但关键是返回bool的判断条件不知道咋写了就。所以,还是直接二分需要的答案,即平均值。
明确了要二分平均值后,那么答案的区间也就很好确定,即0~max(a[1],a[2],...,a[n])。那么大体的二分框架也就有了。
int main() {
scanf("%d%d", &n,&m);
double l = 0, r = 0;
for (int i = 1; i <= n; i++) {
scanf("%d", &a[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;
} printf("%d\n", (int)(r * 1000)); //因为我们找的极大值 所以要右端点*1000 否则可能会出错
return 0;
}
那么关键的就是二分的性质表示也就是check函数的表示。我们需要确保思路的清晰和逻辑的正确。
-
我们的目的是找连续$ \ge f$ 的区间[l,r]使得这段区间的平均数尽可能大,而且我们二分的又是平均是average,那么判断条件就可以表示为:对于一段连续的区间且区间长度大于等于f且平均数不小于当前二分的mid返回TRUE,否则返回false。
-
那么接下来就是如何实现check函数,即如何快速判断出任何一段连续的长度大于等于f的区间内的平均数是否大于等于mid。那么可以二维暴力枚举所有可能的符合条件的区间,但时间会有问题。9/15
-
如何优化一下,这里需要理解一下平均数的原理。一段区间内平均数尽可能大,也就是区间内每个数都减去已知的平均数后,求一遍前缀和,区间前缀和尽可能大。(因为对于每个数来说,就三种情况,大于、等于或者小于平均数,对于一段序列,每个数减去我们所算的平均数,如果大于0,那么他本身就大于平均数,如果小于0,那么它本身就小于平均数)
-
那么问题来了,如何保证我们二分check时是遍历到了所有的$ \ge f$ 的区间?我们完全可以就是按照2中的两个指针二维暴力枚举,但时间确实会TLE。那么有啥性质我们可以优化?回到我们的目的上来,我们的目的是找到一段区间\([l,r]\) 且区间长度$ \ ge f$ ,因为我们的前缀和计算是\(s[r]-s[l-1]\) ,那我们就需要让s[r] 尽可能大,s[l-1] 尽可能小。。而且每一次后指针j后一一个单位,前指针i的可取值范围就从[1,i]成了[1,i+1]。那么就可以如下判断:
bool check(double mid) {
for (int i = 1; i <= n; i++) {
sum[i] = sum[i - 1] + (a[i] - mid); //计算前缀和
}
double minv = 0; //设置最小值
for (int i = 0, j = m; j <= n; j++, i++) {
minv = min(minv, sum[i]); //找最优极小值
if(sum[j] - minv >= 0) return true; //进行判断
} return false; //如果所有的都不满足,那么这个平均数就一定不满足
}
check函数暴力二维枚举版本9/15
#include <bits/stdc++.h>
using namespace std;
const int N = 100010;
int a[N],s[N];
int n,f;
bool check(double mid)
{
for (int i=1;i+f-1<=n;i++)
{
int sum = 0;
double average = 0 ;
for (int j=i+f-1;j<=n;j++)
{
sum=s[j]-s[i-1];
average = 1.0*sum/(j-i+1);
if (average>=mid) return true;
}
}
return false;
}
int main()
{
scanf("%d%d",&n,&f);
double l=0,r=0;
for (int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
r = max(r,(double)a[i]);
s[i]=s[i-1]+a[i];
}
while (r-l>1e-5)
{
double mid = (l+r)/2;
if (check(mid)) l=mid;
else r=mid;
}
printf("%d\n",(int)(r*1000));
return 0;
}
check函数优化使用双指针的版本15/15
#include <bits/stdc++.h>
using namespace std;
const int N = 100010;
int a[N];
double s[N];
int n,f;
bool check(double mid)
{
double m_min=1e9;
for (int i=1;i<=n;i++)
{
s[i]=s[i-1]+a[i]-mid;
}
for (int i=0,j=f;j<=n;i++,j++)
{
m_min = min(m_min,s[i]);
if (s[j] - m_min>=0) return true;
}
return false;
}
int main()
{
scanf("%d%d",&n,&f);
double l=0,r=0;
for (int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
r = max(r,(double)a[i]);
}
while (r-l>1e-5)
{
double mid = (l+r)/2;
if (check(mid)) l=mid;
else r=mid;
}
printf("%d\n",(int)(r*1000));
return 0;
}
二分总结
-
二分的问题,不是一定具备单调性、有序性等性质,最重要的是具备二段性,即一个关键的性质(check函数)使得当前这个值mid使得答案的总区间以mid为界限,一半进行进一步缩小答案范围,一半直接淘汰掉根本不可能有答案。这样才能最终找到边界r,这道题也是深刻体会到了二分的功力。
-
二分一半直接二分最终的答案,而很少二分一些中间变量从而再进行求解和推导,这点需要注意!