博客地址 Nicoppa的个人博客
不懂吗?那就搞懂为止
农夫约翰的农场由 $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
题目题解
先分析这道题应该用什么算法,本题应该怎么入手
首先本题没有出现二分的特征词:“最大值最小” or “最小值最大” 并且给的数列不具备单调性,并且不适于排序,我们看到这种题可以先提出假设 比如,假设这道题用二分能解出
那么我们判断是否存在一个平均值大于等于mid,如果最优解是x,那么mid <= x的时候,必然可以找到一段,其平均值≥mid, 否则 一定找不到
对于二分,二分是二分性而不是单调性 只要满足可以找到一个值一半满足一半不满足即可 而不用满足单调性
那么这个题我们就可以使用二分来解决
知道了这道题的算法之后,我们就来分析这道题
首先我们二分针对的是平均数,那么根据我们就可以捏出以下主函数
int main() {
scanf("%d %d", &n, &m);
double l = 0, r = 0;
for (int i = 1; i <= n; i++) {
scanf("%d", &cows[i]);
r = std::max(r, (double)cows[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函数的写法,我们先来捋一遍思路,防止写代码的时候思路混乱
①:我们要找的是 有没有一段不小于F的区间,使这段区间的平均数尽可能的大,如果我们找到了一段连续的区间且区间长度不小于F且平均数大于我们二分的平均数 那么大于这个数且区间也满足的一定满足了 我们直接判断正确即可
②:因为我们要找一段区间的平均数,根据平均数的一个基本应用,显而易见,对于一段序列,每个数减去我们所算的平均数,如果大于0 那么他本身就大于平均数,如果小于0 那么它本身就小于平均数 此时我们就能算出哪些数大于0 哪些数小于0 ,之后我们再使用前缀和,就能判断一个区间内的平均值是否大于或小于我们二分的平均数了
③:据②我们还可以继续优化,因为我们不仅需要找F大小区间内,我们还要找>F大小区间内的,我们如果用二次for太费时间了,我们这里可以使用双指针的做法,我们设$i = 0, j = F$ 每次使两个数++ 因为$i, j$始终满足相距$F$的距离,所以我们用一个变量$minv$来存储$sum[i]$所遍历到的最小值,这样我们比较的距离一定是$≥F$的,此时若用$sum[j]$去减去$minv$的话,就能得到我们的最优解,如果这个最优解>= 0 那么就满足我们的指定条件(如果不懂这一步 请看②)。 到此,结束
我们便可以写出二分的$check$代码
bool check(double avg) {
for (int i = 1; i <= n; i++) {
sum[i] = sum[i - 1] + (cows[i] - avg); //计算前缀和
}
double minv = 0; //设置最小值
for (int i = 0, j = m; j <= n; j++, i++) {
minv = std::min(minv, sum[i]); //找最优极小值
if(sum[j] - minv >= 0) return true; //进行判断
} return false; //如果所有的都不满足,那么这个平均数就一定不满足
}
以下是全代码:
//#define fre yes
#include <cstdio>
#include <iostream>
const int N = 100005;
int cows[N]; double sum[N];
int n, m;
bool check(double avg) {
for (int i = 1; i <= n; i++) {
sum[i] = sum[i - 1] + cows[i] - avg;
}
double minv = 0;
for (int i = 0, j = m; j <= n; j++, i++) {
minv = std::min(minv, sum[i]);
if(sum[j] - minv >= 0) return true;
} return false;
}
int main() {
scanf("%d %d", &n, &m);
double l = 0, r = 0;
for (int i = 1; i <= n; i++) {
scanf("%d", &cows[i]);
r = std::max(r, (double)cows[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写法(来自蓝书)
看不懂
最小前缀和最大后缀
一语道破!谢谢老哥!
试了一下,算ans的时候好像有点问题,最后得不到正确结果
这不是一样的吗/qd
这个题需要的对二分的理解真的深刻
这个题的难点不在check函数上嘛hh
最后的那个
(r*1000)
很细节,我一开始都是(l*1000)
,我以为都可以来着,但后来一想,l<=mid<=r
,而且又是向下取整,那显然只能r向下取整,而不能是ly总直播课讲了为啥一定要用r,而不是l......
一个问题,为什么最后用l*1000总会差一点,比方说样例就会输出6499
有精度问题 你0.500000和0.499999*1000结果是不一样的
本人已退役 整理好心态后会重新更改博客地址 博客地址现打不开
为什么1e-5变成1e-12就错了呢
我也想不明白
但是1e-5 -6 -7 -8 -9 都是对的,就是从-10开始就不对了
当mid为整数的时候,比如7.0但他实际上是6.9999999999992,l<=mid<=r,然后向下取整答案就错了。
每次check都重新计算吗?直接最小前缀和最小后缀,然后用线段树维护。这样复杂度就是logn了。总的时间复杂度就是O(N)。
大佬,想知道为什么要用浮点数二分啊,设置double类型,
赞!感觉是看过的写的最好的题解之一了
建议将③中的“用一个变量minv来存储i所遍历到的最小值”的i改为sum[i]更清楚
已改
为啥要在check里优化啊,找最值交给二分就行了啊
没有内个找最小前缀的为啥不对
不理解你的意思,能说清楚吗?
我的意思是check里面只找比平均值(mid)大的不行吗,不理解为什么还要找最优极小值
还是不是很清楚,你是不懂 为什么要将“找平均值”这个过程优化吗?
算法题目是有时间复杂度的,二分的时间复杂度是O(n),如果你不将 “找平均值” 这个过程优化,那么就会变成你又需要 O(n) 的时间复杂度去计算平均值,那么就是 O(n^2) 的时间复杂度, O(n^2)的时间复杂度在1s的时间内一般只能在2000-3000的题目中顺利通过,这道题 n 的取值范围是 10w,明显是过不了的 所以需要优化
你好聪明
check函数,好写!
为什么check不能从
开始遍历呢
同学这里我也想不明白, 你搞清楚了吗, 为什么使用前缀和不是从 i = 1的地方开始遍历啊
好像说的 是有个边界问题 你看看没通过那个样例
边界问题,如果i从下标1开始的话,你会忽略掉第一个值,就比如你求区间[l,r]的值用前缀和数组求的话是sum[r]-sum[l-1]。
谢谢
还是不太明白,如果l = 3.999995 mid = 3.99999999…, r = 4.00000000000 那不还是需要l,
r一直维护的不是不符合要求的吗
大佬们,我二分的位置,为啥只能通过12个点呀?有无大佬帮我看看哪里有问题呢?
这个双指针用的让我直呼卧槽
请问r - l 的精度怎么确定如果是r - l > 1e-9就WA了
双指针真妙
这里不是没有判断大于F的情况吗?
牛