Files
python/TangDou/AcWing_TiGao/T6/ErFen/102.md
HuangHai 1f397eca87 'commit'
2025-08-30 18:35:01 +08:00

6.9 KiB
Raw Blame History

##AcWing 102. 最佳牛围栏

一、题目描述

农夫约翰的农场由 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

二、算法分析

1、为什么可以二分?

因为本题求的是 平均值 的最大值,思考一下最大值有 值域范围 吗? 在给定序列,给定$F$的情况下,我们用人脑一个个去计算,肯定最后能拿到一个最大值吧,也就是肯定有解,并且,解是有范围的:

  • 假设所有块都是1,那么平均值最小是1
  • 假设所有块都是2000,那么平均值最大是2000 当然,由于题目明确给出了每个点的数值,所以,上面的$1 \sim 2000$只是一个预估值,但答案肯定在这个范围内。

那本题是不是具有单调性呢? 比如,如果我们猜一个平均值$mid$:

  • 如果这个$mid$小于最大的平均值,那它现在是不是符合某个检查条件呢?
  • 如果这个$mid$大于最大的平均值,那它现在是不是符合某个检查条件呢?

提示:一般在信奥赛的比赛技巧中,遇到平均值,一般考虑将每个位置上的数字都减去这个平均值,然后利用区间和来判断指定区间的平均值与给定平均值的大小关系。

那就是如果每个数字减去平均值后的区间和,如果小于$0$,那么就表示这一个区间的数字平均值小于给定的平均估值,否则就是大于给定的平均估值!

在一个确定大小范围内去猜一个肯定存在的值,这不就是用二分的吗~

2、怎么用二分?

当然是面向答案编程:通过二分猜一个平均数(avg)

  • 类似于最大值最小、平均数最大的问题,一般使用 二分法 对该值进行判定

  • 二分,相当于给原来的问题增加了一个维度的信息,把枚举变成了判断,可以提高检索速度。

思考一下题目的意思:

如果$F$的值,也就是区间的大小,它的取值范围是什么呢?是1<=F<=N,当$F=1$时,那就是整个区间的最大值就是给定的的区间平均最大值。随着$F$的增大,讨论这个问题才有了意义。

题目中说,只有这个取值区间大于$F$就行,看来这个$F$是 一个变化量。

那么,我们需要在所有数字的序列中,去枚举所有可能的开始点和结束点,只要满足$r-l+1>=F$就是一个合理的区间,就有资格参评。

那起点和终点是不是也需要枚举呢?那是肯定的啊:

 for (int k = F; k <= n; k++) { // 枚举区间的右端点
     ....
 }

左端点有哪些呢?画图理解一下:

左端点是从$1$到$k-F+1$都行,再加上枚举左端点的代码,那代码就变成:

 for (int k = F; k <= n; k++) { // 枚举区间的右端点
     for(int L=1;L<=k-F+1;L++){
        ...
     }
 }

枚举了左右端点,也就是枚举了所有可能区间,然后再加上前缀和,就可以拿到一个$O(N^2)$的时间复杂度算法。

但$O(N^2)$还是太慢,因为N<=100000,$O(N^2)=1e10$,需要优化。

我们结合一下题目来分析:

经过上面的变形,估值$avg$的概念已经被消化到$s[]$数组中,我们只要保证 $k$做为终点,$[1 \sim k-F+1]$中 找出一个起点,使得这个区间的前缀和大于等于$0$,就表示,我们在当前$avg$的情况下,找到了一种方案,可以使得区间平均值大于avg!

而$s[k]$是固定不变的,其实事情的本质是在$[1 \sim k-F+1]$中找到一个位置,这个位置的$s[i]$是最小值,因为如果固定值$s[k]$减去这个最小值大于等于$0$,就是合法了,不用再判断其它位置了。

所以,结果从左到右的枚举序,我们惊喜的发现,其实可以只用一层循环,再加一个变量x,随着右端点的不断向右移动,$x$记录$[1 \sim x-F+1]$的最小前缀和就行了。

这个技巧真的好妙,直接将两层循环优化成一层循环了!

bool check(double avg) {
    for (int i = 1; i <= n; i++) s[i] = s[i - 1] + a[i] - avg;
    double x = 0;
    for (int k = F; k <= n; k++) {
        x = min(x, s[k - F]);//
        if (s[k] - x >= 0) return true;
    }
    return false;
}

三、实现代码

#include <bits/stdc++.h>
using namespace std;
const int N = 100010;
const int INF = 0x3f3f3f3f;
int n, F; // n,数字个数,m:区间长度
double a[N], s[N];

bool check(double avg) {
    for (int i = 1; i <= n; i++) s[i] = s[i - 1] + a[i] - avg;
    double x = 0;
    for (int k = F; k <= n; k++) { // 枚举右端点
        /*
        一维前缀和公式: s[l,r]=s[r]-s[l-1]
        左端点的所有可能值l∈[1,k-F+1]
        ① s[l,r]= s[r]-s[0]
        ② s[l,r]= s[r]-s[k-F+1-1]=s[r]-s[k-F]
        */
        x = min(x, s[k - F]);           // 记录左侧不断更新的最小值
        if (s[k] - x >= 0) return true; // 如果s[k]减去前面的最小值大于0,就不必理其它的位置值了
    }
    // 如果所有右端点都枚举完,每个右端点都无法找出左端点最小值,使得区间和大于0,就是都白费
    return false;
}

int main() {
    scanf("%d%d", &n, &F); // F:区间长度

    double l = 2000, r = 0;
    for (int i = 1; i <= n; i++) { // 求前缀和,数组下标从1开始
        scanf("%lf", &a[i]);
        l = min(l, a[i]); // l是平均值的最小值
        r = max(r, a[i]); // r是平均值的最大值
    }

    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;
}