最佳牛围栏

最佳牛围栏

题目描述

在这里插入图片描述


题意解释

给定 n n n [ 1 , 2000 ] [1,2000] [1,2000]中的数,可以任意挑选一段长度 ≥ F \geq F F的连续子段和,让你从这么多的子段中,找出一个平均值最大的子段。然后输出这个平均值。比如题目给的样例是 c o w s [ 10 ] = ( 6 , 4 , 2 , 10 , 3 , 8 , 5 , 9 , 4 , 1 ) cows[10]=(6,4,2,10,3,8,5,9,4,1) cows[10]=(6,4,2,10,3,8,5,9,4,1) F = 6 F=6 F=6。那么我们可以选择 ( 10 , 3 , 8 , 5 , 9 , 4 ) (10,3,8,5,9,4) (10,3,8,5,9,4)这个子段,这个子段中包含了 F = 6 F=6 F=6个数,这个子段的平均值是 ( 10 + 3 + 8 + 5 + 9 + 4 ) 6 = 6.5 \dfrac {(10+3+8+5+9+4)}{6}=6.5 6(10+3+8+5+9+4)=6.5,然后题目要求平均值的最大值要乘以 1000 1000 1000然后向下取整,那么最终结果是: 6.5 × 1000 = 6500 6.5\times 1000=6500 6.5×1000=6500。但是并不一定是说要取 F = 6 F=6 F=6,也可以让 F > 6 F>6 F>6,只要满足 ≥ F \geq F F即可,但是假设我们选择了取 7 7 7,那么此时这个子段就是: ( 10 , 3 , 8 , 5 , 9 , 4 , 1 ) (10,3,8,5,9,4,1) (10,3,8,5,9,4,1),其平均值是 ( 10 + 3 + 8 + 5 + 9 + 4 + 1 ) 7 = 5.7 \dfrac {(10+3+8+5+9+4+1)}{7}=5.7 7(10+3+8+5+9+4+1)=5.7,但是显然此时这个子段的平均值小于 F = 6 F=6 F=6子段的平均值。因为牛的数量是从1到2000,那么平均值一定是在2000以内。所以 l l l r r r的区间是从 1 1 1 2000 2000 2000


核心思路

先用实数二分算法求出一个 m i d mid mid,然后假设这个 m i d mid mid就是最大的平均值,然后在序列中寻找是否有平均值是 ≥ m i d \geq mid mid并且区间长度 ≥ F \geq F F的子段,如果我们找到了一段连续的区间长度 ≥ F \geq F F并且平均值大于等于我们二分出来的平均数,不妨设该区间的平均值是 x ‾ \overline x x x ‾ ≥ m i d \overline x \geq mid xmid,那么如果某些子段区间,它的长度 ≥ F \geq F F并且它的平均值是 ≥ x ‾ \geq \overline x x,那么这些子段区间也一定是满足条件的,对于这些满足条件的子段区间,我们直接判定正确即可。

问题:为什么这道题可以用二分算法呢?

其实这道题,对于平均值来说,它是具有单调性的,只是很难发现而已。只要具有单调性,那么就一定可以用二分算法。由于是求平均值,那么答案就可能是小数,因此我们是采用实数二分,而不是整数二分。注意,是我们要找到这些平均值具有单调性,而不是说题目输入的原始数据具有单调性哦!!!

在这里插入图片描述

  • 利用二分查找平均值,计算当前平均值是否可行,如果可行,则向右收缩空间,尝试寻找更大的平均值。反之若当前平均值不行,则向左收缩空间。为什么可行后就要向右收缩呢?这里可以用右侧边界来理解,因为我们的目标是寻找第一个大于等于 m i d mid mid的平均值,而这正好与右侧边界的含义相吻合。

  • 因为我们要找的是某些连续子段区间的平均值,要求这个平均值必须 ≥ \geq 当前二分出来的 m i d mid mid,即我们想要证明的是: ( a [ 1 ] + a [ 2 ] + ⋯ + a [ n ] ) n ≥ m i d \dfrac {(a[1]+a[2]+\cdots +a[n])}{n}\geq mid n(a[1]+a[2]++a[n])mid,先等式两边同时乘上 n n n,然后等式两边再同时加上 − n ∗ m i d -n*mid nmid,把 − n ∗ m i d -n*mid nmid看作是 n n n − m i d -mid mid相乘,把这 n n n − m i d -mid mid分配个 a [ 1 ] , a [ 2 ] , ⋯   , a [ n ] a[1],a[2],\cdots ,a[n] a[1],a[2],,a[n],最终可转换为: ( a [ 1 ] − m i d ) + ( a [ 2 ] − m i d ) + ⋯ + ( a [ n ] − m i d ) ≥ 0 (a[1]-mid)+(a[2]-mid)+\cdots +(a[n]-mid)\geq 0 (a[1]mid)+(a[2]mid)++(a[n]mid)0。其实也可以这么理解,要想判定一个区间的平均值是否大于等于某个数 m i d mid mid,那么只需要这个区间中的每一个数都大于等于 m i d mid mid即可,也就等价于这个区间中的每一个数都减去 m i d mid mid,如果大于0,那么这个数本身就是大于 m i d mid mid,如果小于0,那么这个数本身就是小于 m i d mid mid,让这个区间中的每一个数都减去 m i d mid mid后,我们再让序列中减去 m i d mid mid后的这些数累加,看看累加的和是否 ≥ 0 \geq 0 0,如果 ≥ 0 \geq 0 0,那么就能说明这段区间的平均值就是 ≥ m i d \geq mid mid,否则就说明这段区间的平均值是小于 m i d mid mid的。那么该子段区间就是无效的。因此,我们想要查找某些连续子段区间的平均值是否大于等于 m i d mid mid,也就等价于求这个区间中的每一个数减去 m i d mid mid然后再累加,看看累加和是否 ≥ 0 \geq 0 0,即看看累加和是否非负。而区间的累加和,我们可以借助前缀和来求解。

  • 设前缀和数组为 s u m sum sum,则我们要找到一个子段 [ i , j ] ( j − i ≥ F ) , s u m [ j ] − s u m [ i ] ≥ 0 [i,j](j-i\geq F),sum[j]-sum[i]\geq 0 [i,j](jiF),sum[j]sum[i]0,如果满足,则说明当前二分的这个 m i d mid mid是正确的, m i d mid mid的右边可能存在更大的平均值,那么我们就让 l = m i d l=mid l=mid,寻找右侧边界,继续判断更大的平均值,否则就设置 r = m i d r=mid r=mid缩小范围。

  • 由于数据是 1 0 5 10^5 105级别,显然通过枚举 i , j i,j i,j的做法会超时 O ( n 2 ) O(n^2) O(n2)。事实上我们可以做等价转换,由数学意义可知,要想证明存在 s u m [ j ] − s i m [ i ] ≥ 0 sum[j]-sim[i]\geq 0 sum[j]sim[i]0,即 s u m [ j ] ≥ s u m [ i ] sum[j]\geq sum[i] sum[j]sum[i],那么只要证明 s u m [ j ] sum[j] sum[j]大于等于 s u m [ i ] sum[i] sum[i]的最小值就好了。即: ∃ ( s u m [ j ] ≥ s u m [ i ] )    ⟹    s u m [ j ] ≥ m i n ( s u m [ k ] , 0 ≤ k ≤ i ) \exist (sum[j]\geq sum[i])\implies sum[j]\geq min(sum[k],0\leq k\leq i) (sum[j]sum[i])sum[j]min(sum[k],0ki)。利用双指针 i , j i,j i,j便可以在 O ( n ) O(n) O(n)内检查是否由满足条件的序列。初始时 i = 0 , j = F i=0,j=F i=0,j=F,每次使 i i i j j j++,因为 i i i j j j始终满足 ≥ F \geq F F的距离,并设置 m i n V = m i n ( s u m [ k ] , 0 ≤ k ≤ i ) minV=min(sum[k],0\leq k\leq i) minV=min(sum[k],0ki),如果我们用 j j j所指位置的前缀减去 m i n V minV minV,就能得到最优解。每次迭代时更新 m i n V minV minV并判断当前子段是否非负,如果当前子段是非负的,那么就是满足要求的, r e t u r n t r u e return \quad true returntrue即可。

在这里插入图片描述

问题:为什么二分之后不用l而是用r呢?

精度问题。如果答案是6500,那么l的取值虽然可以和r无限接近,但永远小于6500,所以取整之后就会得到6499,就错了。对于整数二分来说,二分结束后l和r是相同的,但是对于实数二分来说,由于存在精度问题,因此l和r不一定相等,由判断条件也可以知道,整数二分的循环条件是l<r,那么结束时l==r;实数二分的循环条件是l+eps<r,那么结束条件是l+eps>=r,而且l和r不会相等,它俩之间无限接近,但是相差了无穷小的eps的距离。由于是求最大值,而r>l,因此我们需要尽量的选择最大的,那么就是选择r而不是l。

在这里插入图片描述


代码

#include<iostream>
#include<cmath>
using namespace std;
const int N=100010;
int n,F;
int cows[N];
double sum[N];
//判断avg这个点是否满足该性质,即是否在绿色区间中
bool check(double avg)
{
    //预处理出原始的数据-平均值后的前缀和,而不是原始数据的前缀和
    for(int i=1;i<=n;i++)
    sum[i]=sum[i-1]+cows[i]-avg;
    double minV=0;	//0~i中最小的那个sum[i]
    //找到0~i中最小的那个sum[i]
    for(int i=0,j=F;j<=n;i++,j++)
    {
        minV=min(minV,sum[i]);
        if(sum[j]>=minV)
        return true;
    }
    return false;
}
int main()
{
    cin >>n>>F;
    for(int i=1;i<=n;i++)
    cin >>cows[i];
    double l=1,r=2000;
    //题目说要乘1000,也就是说需要保留3位小数
  //实数的二分中设置的精度eps有个规则:位数n=要保留的小数位数+2
    //精度1e-n是固定的,因此这里n=3+2=5,于是精度就设置为1e-5
    while(l+1e-5<r)
    {
        double mid=(l+r)/2;
        //判断这个点mid是否满足我们设定的性质,即是否在mid是否落在绿色区间中
        //如果满足,由于是求右侧边界,那么让l=mid
        if(check(mid))
        l=mid;
        else
        r=mid;
    }
    //(int)表示向下取整
    printf("%d\n",(int)(r*1000));
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

卷心菜不卷Iris

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值