HDU2993 MAX average problem [斜率dp]

题意:给你一段长度为n的数列, 求其长度不小于 K 的平均值最大的子串。

解析:抄的解析http://www.docin.com/p-469236754.html 例二

[分析]

简单的枚举算法可以这样描述:每次枚举一对满足条件的(a, b),即ab-F+1,检查ave(a, b),并更新当前最大值。

然而这题中N很大,N2的枚举算法显然不能使用,但是能不能优化一下这个效率不高的算法呢?答案是肯定的。

目标图形化

首先一定会设序列ai的部分和:Si=a1+a2+…+ai,特别的定义S0=0。

这样可以很简洁的表示出目标函数公式

如果将S函数绘在平面直角坐标系内,这就是过点Sj和点Si-1直线的斜率!

于是问题转化为:平面上已知N+1个点,Pi(i, Si),0≤iN,求横向距离大于等于F的任意两点连线的最大斜率。

构造下凸折线

有序化一下,规定对i<j,只检查Pj向Pi的连线,对Pi不检查与Pj的连线。也就是说对任意一点,仅检查该点与在其前方的点的斜率。于是我们定义点Pi的检查集合为

Gi = {Pj, 0≤ji-F}

特别的,当i<F时,Gi为空集。

其明确的物理意义为:在平方级算法中,若要检查ave(a, b),那么一定有Pa∈Gb;因此平方级的算法也可以这样描述,首先依次枚举Pb点,再枚举Pa∈Gb,同时检查k(PaPb)。

若将Pi和Gi同时列出,则不妨称Pi为检查点,Gi中的元素都是Pi的被检查点。

当我们考察一个点Pt时,朴素的平方级算法依次选取Gt中的每一个被检查点p,考察直线pPt的斜率。但仔细观察,若集合内存在三个点Pi, Pj, Pk,且i<j<k,三个点形成如下图所示的的关系,即Pj点在直线PiPk的上凸部分:k(Pi, Pj)>k(Pj, Pk),就很容易可以证明Pj点是多余的。

图2

图 2

若k(Pt, Pj) > k(Pt, Pi),那么可以看出,Pt点一定要在直线PiPj的上方,即阴影所示的1号区域。同理若k(Pt, Pj) > k(Pt, Pk),那么Pt点一定要在直线PjPk的下方,即阴影所示的2号区域。

综合上述两种情况,若PtPj的斜率同时大于PtPi和PtPk的,Pt点一定要落在两阴影的重叠部分,但这部分显然不满足开始时t>j的假设。于是,Pt落在任何一个合法的位置时,PtPj的斜率要么小于PtPi,要么小于PtPk,即不可能成为最大值,因此Pj点多余,完全可以从检查集合中删去。

这个结论告诉我们,任何一个点Pt的检查集合中,不可能存在一个对最优结果有贡献的上凸点,因此我们可以删去每一个上凸点,剩下的则是一个下凸折线。最后需要在这个下凸折线上找一点与Pt点构成的直线斜率最大——显然这条直线是在与折线相切时斜率最大,如图所示。

图3

图 3

维护下凸折线

这一小节中,我们的目标是:用尽可能少的时间得到每一个检查点的下凸折线。

算法首先从PF开始执行:它是检查集合非空的最左边的一个点,集合内仅有一个元素P0,而这显然满足下凸折线的要求,接着向右不停的检查新的点:PF+1, PF+2, …, PN

检查的过程中,维护这个下凸折线:每检查一个新的点Pt,就可以向折线最右端加入一个新的点Pt-F,同时新点的加入可能会导致折线右端的一些点变成上凸点,我们用一个类似于构造凸包的过程依次删去这些上凸点,从而保证折线的下凸性。由于每个点仅被加入和删除一次,所以每次维护下凸折线的平摊复杂度为O(1),即我们用O(N)的时间得到了每个检查集合的下凸折线。

最后的优化:利用图形的单调性

最后一个问题就是如何求过Pt点,且与折线相切的直线了。一种直接的方法就是二分,每次查找的复杂度是O(log2N)。但是从图形的性质上很容易得到另一种更简便更迅速的方法:由于折线上过每一个点切线的斜率都是一定的[1],而且根据下凸函数斜率的单调性,如果在检查点Pt时找到了折线上的已知一个切点A,那么A以前的所有点都可以删除了:过这些点的切线斜率一定小于已知最优解,不会做出更大的贡献了。

于是另外保留一个指针不回溯的向后移动以寻找切线斜率即可,平摊复杂度为为O(1)。

至此,此题算法时空复杂度均为O(N),得到了圆满的解决。

 


[1] 由于折线没有连续性,因此更准确的应该说,过每一个点切线斜率的范围都一定的。

 

N(log2N) 和  O(N)的两种算法的代码如下

N(log2N) 算法

#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;

const int N = 100005;
int sum[N];

struct point{
    point(){};
    point(int x, int y) : x(x),y (y){}
    int x, y;
}s[N];

long long cross(point a, point b, point c) { //ab * bc
    return (long long)(b.x - a.x) * (c.y - b.y) - (long long)(c.x - b.x) * (b.y - a.y);
}

int bsearch(int l, int r, point &now) {
    while(l < r){
        int mid = (l + r) >> 1;
        if(cross(s[mid], s[mid + 1], now) < 0)  r = mid;
        else l = mid + 1;
    }
    return l;
}

double solve(int n, int m){
    double maxx = 0;
    int p = -1;
    for(int i = m; i <= n; i ++){
        point r(i - m, sum[i - m]), now(i, sum[i]);
        while(p > 0 && cross(s[p - 1], s[p], r) < 0) p --;
        s[++p] = r;
        int opt = bsearch(0, p, now);
        double ans = (double)( sum[i] - s[opt].y ) / (i - s[opt].x);
        if(ans > maxx) maxx = ans;
    }
    return maxx;
}
void read(int &r) {
    char c;
    while (c = getchar(), c < '0' || c > '9');
    r = c - '0';
    while (c = getchar(), c >= '0' && c <= '9') r = r * 10 + c - '0';
}
int main(){
    int n, m, i;
    while(~scanf("%d%d", &n, &m)){
        sum[0] = 0;
        for(i = 1; i <= n; i ++){
            //read(sum[i]);
            scanf("%d", &sum[i]);
            sum[i] += sum[i - 1];
        }
        printf("%.2f\n", solve(n, m));
    }
}

O(N)算法

#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;

const int N = 100005;
int sum[N];

struct point{
    point(){};
    point(int x, int y) : x(x),y (y){}
    int x, y;
}s[N];

long long cross(point a, point b, point c) { //ab * bc
    return (long long)(b.x - a.x) * (c.y - b.y) - (long long)(c.x - b.x) * (b.y - a.y);
}

double solve(int n, int m){
    double maxx = 0;
    int p = -1, h = 0;
    for(int i = m; i <= n; i ++){
        point r(i - m, sum[i - m]), now(i, sum[i]);
        while(p > h && cross(s[p - 1], s[p], r) < 0) p --;
        s[++p] = r;
        while(h < p && (long long)(sum[i] - s[h].y) * (i - s[h + 1].x) < (long long)(sum[i] - s[h + 1].y) * (i - s[h].x) ) h ++;
        double ans = (double)( sum[i] - s[h].y ) / (i - s[h].x);
        if(ans > maxx) maxx = ans;
    }
    return maxx;
}
void read(int &r) {
    char c;
    while (c = getchar(), c < '0' || c > '9');
    r = c - '0';
    while (c = getchar(), c >= '0' && c <= '9') r = r * 10 + c - '0';
}
int main(){
    int n, m, i;
    while(~scanf("%d%d", &n, &m)){
        sum[0] = 0;
        for(i = 1; i <= n; i ++){
            read(sum[i]);
            //scanf("%d", &sum[i]);
            sum[i] += sum[i - 1];
        }
        printf("%.2f\n", solve(n, m));
    }
}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值