【斜率优化】HDU-2993——MAX Average Problem

我与影子孤独终老i 提交于 2019-12-22 05:39:39

前言

大约一年前,我在机房学“斜率优化”

一年后的今天,我在机房学“斜率优化”

... ...

题目

Consider a simple sequence which only contains positive integers as a1, a2 ... an, and a number k. Define ave(i,j) as the average value of the sub sequence ai ... aj, i<=j. Let’s calculate max(ave(i,j)), 1<=i<=j-k+1<=n.

Input

There multiple test cases in the input, each test case contains two lines.
The first line has two integers, N and k (k<=N<=10^5).
The second line has N integers, a1, a2 ... an. All numbers are ranged in [1, 2000].

Output

For every test case, output one single line contains a real number, which is mentioned in the description, accurate to 0.01.

Sample Input

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

Sample Output

6.50

题目大意

读入N个正整数a[1]...a[n],和一个整数k。

定义区间平均值ave(i, j)表示一个连续子区间a[i]...a[j]的元素的平均值。

求最大的区间平均值max(ave(i, j)) 1<=i<=j-k+1<=n.

分析

此乃~斜率优化板题也~

参考博客&特别鸣谢:https://www.cnblogs.com/Free-rein/archive/2012/08/09/2630190.html


解题思路

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

   这样可以很简洁的表示出目标函数ave(i,j)=(Sj-S(i-1))/(j-(i-1))!

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

于是问题转化为:平面上已知N+1 个点,Pi(i, Si),0≤i≤N,求横向距离大

于等于F的任意两点连线的最大斜率。


构造下凸折线

   有序化一下,规定对i<j,只检查Pj向Pi的连线,对Pi不检查与Pj的连线。

也就是说对任意一点,仅检查该点与在其前方的点的斜率。于是我们定义点Pi

的检查集合为Gi = {Pj, 0≤j≤i-F},特别的,当i<F时,Gi为空集

   其明确的物理意义为:在平方级算法中,若要检查ave(a, b),那么一定有Pa∈Gb;

因此平方级的算法也可以这样描述,首先依次枚举Pb点,再枚举Pa∈Gb,同时检查k(PaPb)。//k为斜率


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

   当我们考察一个点Pt时,朴素的平方级算法依次选取Gt中的每一个被检查点p,

考察直线pPt的斜率。但仔细观察,若集合内存在三个点Pi, Pj, Pk,且i<j<k,三个点形成如下图

所示的的关系,即Pj点在直线PiPk的上凸部分:k(Pi, Pj)>k(Pj,Pk),就很容易可以证明Pj点是多余的。

证明:

(一)若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 点构成的直线斜率最大——显然这条直

线是在与折线相切时斜率最大,如图所示。


维护下凸折线——新元素入队的处理

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

     算法首先从PF开始执行:它是检查集合非空的最左边的一个点,集合内仅有一个元素P0,

而这显然满足下凸折线的要求,接着向右不停的检查新的点:PF+1,PF+2, …, PN。

 

     检查的过程中,维护这个下凸折线:每检查一个新的点Pt,就可以向折线最右端加入一个新的点Pt-F,

同时新点的加入可能会导致折线右端的一些点变成上凸点,我们用一个类似于构造凸包的过程依次删去这些上凸点,

从而保证折线的下凸性。由于每个点仅被加入和删除一次,所以每次维护下凸折线的平摊复杂度为O(1),

即我们用O(N)的时间得到了每个检查集合的下凸折线。


最后的优化:利用图形的单调性——寻找、更新答案的处理

     最后一个问题就是如何求过Pt点,且与折线相切的直线了。一种直接的方法就是二分,每次查找的复杂度是O(log2N)。

但是从图形的性质上很容易得到另一种更简便更迅速的方法:

由于折线上过每一个点切线的斜率都是一定的,而且根据下凸函数斜率的单调性,如果在检查点Pt 时找到了折线上的已知一个切点A,

那么A以前的所有点都可以删除了:过这些点的切线斜率一定小于已知最优解,不会做出更大的贡献了。

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

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


总结——斜率优化的几大步骤(蒟蒻的大白话解释)

1.分析题目,列出转移方程或要求的“目标物”的式子

2.将方程或式子写作“y=kx+b”有时是“y/x=k”的形式,即直线表达式

3.维护一个双端队列,先处理队尾,再处理队首(决策队尾、队首元素的去留)

(“先队首再队尾导致错误”的具体例子请见:https://blog.csdn.net/qq_36294918/article/details/103641669

4.转移方程或更新答案

5.输出答案,快乐结束


特别感谢:

《浅谈数形结合思想在信息学竞赛中的应用》---周源

提供了许多关于数形结合解题的思路。

代码

#include<cstdio>
#include<cstring>
#include<cmath>
#include<iostream>
#include<algorithm>
using namespace std;
const int MAXN=1e5;
double sum[MAXN+5];
int q[MAXN+5];
int n,len;
inline char __getchar()
{
	static char buf[100000],*p1=buf,*p2=buf;
	return p1==p2&&(p2=(p1=buf)+fread(buf,1,100000,stdin),p1==p2)?EOF:*p1++;
}
inline bool qread(int &x)
{
	char ch=__getchar();
	if(ch==EOF)return false;
	x=0;
	while(!(ch>='0'&&ch<='9'))ch=__getchar();
	while(ch>='0'&&ch<='9')x=x*10+ch-'0',ch=__getchar();
	return x;
}
void Solve()
{
	memset(q,0,sizeof(q));
	double Max=0;
	int head=0,tail=0;
	for(int i=len;i<=n;i++)
	{
		int now=i-len;//长度至少为k 
		//处理队尾 
		while(head<tail)
		{
			double k1=(sum[q[tail]]-sum[q[tail-1]])/(q[tail]-q[tail-1]);
			double k2=(sum[now]-sum[q[tail]])/(now-q[tail]);
			//保证下凸 
			if(k1>=k2)
				tail--;//去掉上凸的点 
			else
				break;
		}
		//入队 
		tail++;
		q[tail]=now;
		//处理队首 
		while(head<tail)
		{
			double k1=(sum[q[head]]-sum[i])/(q[head]-i);
			double k2=(sum[q[head+1]]-sum[i])/(q[head+1]-i);
			//去掉不优的点 
			if(k1<=k2)
				head++;//下凸线中去掉斜率较小的点(从左往右)
			else
				break;
		} 
		double k=(sum[q[head]]-sum[i])/(q[head]-i);
		Max=max(Max,k);
	}
	printf("%.2f\n",Max);
}
int main()
{
	while(qread(n)&&qread(len))
	{
		sum[0]=0;
		for(int i=1;i<=n;i++)
		{
			int x;
			qread(x);
			sum[i]=sum[i-1]+x;
		}
		Solve();
	}
	return 0;
}

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!