Best Cow Fence -----TYWZOJ.TOP(#6015)

题目描述:

农场主 John (简称 FJ) 的农场有一长排的 N (1 <= N <= 100,000)块地组成. 每块地有一定数量 (ncows) 的牛, 1 <= ncows <=2000.

FJ 想修建环绕邻接的一组地块的栅栏, 以最大化这组地块中平均每块地中牛的个数.

这组地块必须包含至少 F (1 <= F <= N) 块地, F 作为输入给出.

给定约束, 计算出栅栏的布置情况以最大化平均数.

** 友情提示:由于本题过于陈旧,数据上有一些偏差,请在解答的时候精度设为"1e-5". XAX。

输入格式:

第一行: 空格分隔的两个整数, N 和 F.

第2到第N+1行: 每行包含一个整数, 一块地中的牛数. 行 2 给出地块 1 中的牛数, 行 3 给出地块 2 中的牛数, ...

输出格式:

一行一个整数, 它是最大平均数的 1000 倍.

不要用舍入求整, 仅仅输出整数。

样例

input

10 6
6 
4
2
10
3
8
5
9
4
1

output:

6500

数据范围与提示←你绝对看不见

时间限制:1s

空间限制:30000KB

本题大意)_(大意是说,给你一个正整数序列,找出一个区间使得平均值最大,要求该区间的长度大于等于F。(很容易理解对不对?)

看完题之后,基本上就知道是做不出来的了。只想得到那种最简单的O(N^2)的解法,但是N = 100,000。这种解法必然超时。(本蒟蒻就逊于这个点上多次

这里推荐两种解法——正文开始

一:二分法法子

我们可以比较容易得出答案的最大值和最小值,即为序列中最大元素和最小元素。
二分法的关键在于判断“一个可能的解跟正确答案相比是大了还是小了”。方法是:
如果要判断val这个解,那就让序列里所有元素的值都减去val。
然后试图寻找一段连续的区间,该区间的长度大于F,并且区间大于0。
可见,问题一下转化成统计数字的和,而不是数字的平均值,问题变得明朗了。
寻找这种区间的算法是一个很简单的动态规划,复杂度为O(N)。
用 f[a, b] 表示在区间 [a, b] 中,所有子区间的最大值。

伪代码:

当 b - a = F 时,f[a, b] 为序列中对应的和。
当 b - a > F 时,f[a, b] = max{ f[a, b - 1] + arr[b], f[b - f + 1, b] }

我们要求的是 f[0, N]。
因此,二分法的复杂度是 O(NlogN)。代码跑了接近200ms。(好慢呀!)

那么怎么尽快验证答案呢?

想象一个情景:一个固定左端起点长度为F的区间向右生长,直至平均值对大。循环n次来更换不同起点,所有情况都能考虑周全。

这里需要优化。首先,知道平均值要知道总和,长度为F时的和可以用前缀和一步得到。

向右延长这步也可以类似去做。我们要首先把所有整数都减去当前的候选答案。这一步很关键,因为我们要获取的是 平均值的最大,减去后,我们便可以直接使用确定起点向右的数字的和作为状态,而不用再考虑除掉个数来计算对平均值的影响。

定义t[i]为以i为起点,向右延长的最大和。如果t[i+1]>=0,那么t[i]=t[i+1]+w[i]。若t[i+1]<0,那么不如扔掉i+1及其之后的数字,故此时t[i]=w[i]。

这样,能保证O(n)时间内验证候选答案。(感谢博客园的大佬~\(≧▽≦)/~)

上代码!开饭了!

#include <bits/stdc++.h>//不加万能头,万错一个透。加了万能头,万题迎面透。
#define MAX_N 100032
double S[MAX_N], A[MAX_N];
int N, F;
int check(double val){//简单先过一遍
    double cur, pre;
    int i;
    pre = S[F - 1] - val * (F - 1);
    for (i = F; i <= N; i++) {
        cur = S[i] - S[i - F] - val * F;
        pre = pre + A[i] - val;
        if (cur > pre)
            pre = cur;
        if (pre > -1e-6)//注意范围
            return 1;
    }
    return 0;
}
int main(){//前缀和の饭
    int i;
    double l, r, m;
    scanf("%d%d", &N, &F);
    l = 1e-5;//范围++
    r = 0;
    A[0] = S[0] = 0;
    for (i = 1; i <= N; i++) {
        scanf("%lf", &A[i]);
        if (A[i] > r)
            r = A[i];
        if (A[i] < l)
            l = A[i];
        S[i] = S[i - 1] + A[i];
    }
    while (r - l >= 1e-6) {//大了一点,需要接纳
        m = (l + r) / 2;
        if (check(m))
            l = m;
        else
            r = m;
    }
    printf("%d", (int)(r * 1000));
    return 0;//养成好习惯
}

感谢白嫖

谢谢观看

|

|

|

|

|

|

|

|

|

|

|

|

|

|

|

|

|

|

|

|

|

|

|

|

|

|

|

|

|

|

|

|

|

|

|

|

我们继续!

二、神奇凸包法(大佬的思路——SSD小萌新)

凸包法大概简述:

凸包问题可以描述为:给定一个点集P,求最小点集S,使得S构成的形状能包含P[1]。一般的研究主要针对二维平面上和三维空间上的凸包,因为他们在更多的应用中能发挥作用。

凸包的定义为:平面的一个子集S被称为是“凸”的,当且进当对于任意两点p,q∈S,线段 都完全属于S。几何S的凸包CH(S),就是包含S的最小凸集,更准确地说,它是包含S的所有凸集的交[2]。由此还可以推出凸包的很多性质,包括一条直线如果与凸包相交(不是相切)的话,最多交于两条边或者两个面。。

本文中,我们假设:凸包上的每个平面的法向量朝外,或者说都是半平面;因此某个点到面对距离有可能为负,当p点和面的法向量不处在同一侧时,p点到面的距离为负,距离计算公式为点点乘平面法向量在加上平面d值。此外,所求得到凸包为流形,1条边最多只能连接2个面。——SSD小萌新大佬行为

方法详解:

这个方法不是真的求点的凸包,是用了求凸包时候的技巧。
首先把序列转化成一个图,一共有N个点,第 i 个点的坐标为 (i, S[i]),其中 S[i] 为序列的前 i 项和。
在图上,能观察到,点a点b之间的斜率就是区间[a, b]的平均值。
当 N = 6, F = 3 的时候,按照最简单的 O(N^2) 的做法,计算每两个点之间的斜率,计算的顺序为:
[1, 3]
[1, 4] [2, 4]
[1, 5] [2, 5] [3, 5]
[1, 6] [2, 6] [3, 6] [4, 6]
在算第6个点的时候,依次算了1,2,3,4跟点6的斜率。
为了避免不必要的计算,我们要没必要计算的点剔除。
用类似凸包的计算更新方法,在点1,2,3。。。中维护一条“下凸折线”。
这样,可以保证末尾的点跟折线中的点的斜率是先递增再递减的关系。
就能比较快的找出最大的斜率了。
这个算法的复杂度,网上的人说是O(N),但我觉得好像不是O(N)啊,也不知道是什么。
但是,绝对不能单单以复杂度来评价算法的啦。
代码跑了150ms左右。比2分的还是快一点。

 

#include <bits/stdc++.h>
#define MAX_N 100032
int S[MAX_N], stack[MAX_N], N, F, sp;
__inline int turn_right(int a, int b, int c){
    int x1, y1, x2, y2;
    x1 = b - a;
    y1 = S[b] - S[a];
    x2 = c - b;
    y2 = S[c] - S[b];
    return x1*y2 - x2*y1 <= 0;
}
__inline double calc_k(int a, int b){
    return (double)(S[b] - S[a]) / (double)(b - a);
}
int main(){
    int i, j;
    double max_val, val;
    scanf("%d%d", &N, &F);
    for (i = 1; i <= N; i++) {
        scanf("%d", &j);
        S[i] = S[i - 1] + j;
    }
    max_val = 0;
    for (i = 0; i <= N - F; i++) {
        while (sp >= 2 && turn_right(stack[sp - 2], stack[sp - 1], i))
            sp--;
        stack[sp++] = i;
        for (j = sp; 
             j >= 2 && turn_right(stack[j - 2], stack[j - 1], i + F);
             j--
             );
        val = calc_k(stack[j - 1], i + F);
        if (val > max_val)
            max_val = val;
    }
    printf("%d", (int)(max_val * 1000));
    return 0;
}

但是!

有一个点没有过!

所以我们可以直接暴力输出!怎么行呢

我们可以把它的主函数中的循环更改一哈!

把它KA掉后我们就可以在仅仅用了154ms时间过了!再次感谢SSD小萌新大佬!

最后的最后!

感谢大家的白嫖!都暗示到这个份上了,不表示一下么?

上一篇:P7990-[USACO21DEC]Closest Cow Wins S【堆,贪心】


下一篇:基于Spring MVC + Spring + MyBatis的【学生信息管理系统】