题解 P3299 保护出题人 (斜率转化凸包+三分)

591 阅读3分钟

题目源自洛谷

题目大意:

模拟植物大战僵尸的一排,可以理解成豌豆射手对第一个僵尸发射激光,造成持续伤害并且第一个僵尸死后第二个僵尸立马收到伤害。 然后不同的是,有n个回合,每回合在僵尸队列前端会添加一个僵尸,之后的僵尸会往后移动d个距离,n个回合单独计算。求n个回合杀死所有僵尸的最小攻击力(hp/s)。

题目讲解:

首先模拟一下样例,贪心的求:在第i个回合,能杀死前j个僵尸的攻击力,取最大值;数学表达就是

max(sumisumj1xi+(ij)d)max(\frac{sum_i-sum_{j-1}}{x_i+(i-j)*d})

得到公式后,很显然,可以转化为两点求斜率的式子:

sumisumj1xi+idjd\frac{sum_i-sum_{j-1}} {x_i+i*d-j*d}

即点(xi+id,sumi)(x_i+i*d,sum_i)(jd,sumj1)(j*d,sum_{j-1})

可以发现对于每回合,第一个点是唯一的,第二类点的个数是随i增大而增多的,但也都是固定的。

这时候此题已经转化为了对于每回合,求第一个点与前i个第2类点连成线段斜率的最大值。

看图我们可以大致猜测一个结论:斜率最大值的点一定在第二类点构成的下凸壳上; 之后我们来简单证明(感谢同校大佬提供的思路,%%%Daowuu):

如图是i=5的时候的第一种点(E) 和第二类点(A、B、C、D),尝试将E也加入凸包中,假设与E相连的点为P,那么与E连线斜率最大的点一定是点P。因为根据凸包的性质可知其他点一定在与PE这条边的内侧,即其他点与E连线的斜率一定小于PE的斜率,证毕。

而此时,凸壳上的点会被分成两部分,一部分是在E加入凸壳后仍然在凸壳边上的点,这些点(x坐标小于P点)的斜率一定小于E,而E加入后可能会弹出一部分点(x坐标大于P点),这些点的斜率也是小于PE的,此时我们会发现在原凸壳上,与E连线的斜率是一个单峰函数。 最后采用三分寻找P点即可。 时间复杂度:O(nlogn)O(nlogn) 保护出题人 代码:

#include <bits/stdc++.h>
#define int long long
#define eps 0.00000001
using namespace std;

struct Point{
	double x,y;
	Point(double x=0,double y=0):x(x),y(y){}
};
Point stk[100005];
typedef Point Vector;

Vector operator + (Vector A, Vector B) {return Vector(A.x+B.x,A.y+B.y);}
Vector operator - (Vector A, Vector B) {return Vector(A.x-B.x,A.y-B.y);}
Vector operator * (Vector A, double B) {return Vector(A.x*B,A.y*B);}
Vector operator / (Vector A, double B) {return Vector(A.x/B,A.y/B);}

int dcmp(double x)
{
	if(fabs(x)<eps) return 0;
	else if(x>0) return 1;
	else return -1;
}

double cross(Vector p,Vector q){return p.x*q.y-p.y*q.x;}
int n,d,cnt=0;
double hp,pos,pre[100005];

signed main()
{
	scanf("%lld%lld",&n,&d);
	double res=0;
	for(int i=1;i<=n;i++)
	{
		scanf("%lf%lf",&hp,&pos);
		pre[i]=pre[i-1]+hp;
		Point p={i*1.0*d,pre[i-1]},now={pos+i*1.0*d,pre[i]};

		while(cnt>1&&dcmp(cross(stk[cnt]-stk[cnt-1],p-stk[cnt]))<0) cnt--;
		cnt++;
		stk[cnt]=p;

		if(cnt==1){
			res+=(hp/pos);
		}else{
			int left=1,right=cnt;
			while(left+10<right)
			{
				int mid=(left+right)/2;
				int midr=(mid+right)/2;
				double k1=(now.y-stk[mid ].y)/(now.x-stk[mid ].x);
				double k2=(now.y-stk[midr].y)/(now.x-stk[midr].x);

				if(k1>k2) right=midr;
				else if(k1<k2) left=mid;
			}

			double maxd=0;
			for(int j=left;j<=right;j++)
			{
				double temp=(now.y-stk[j].y)/(now.x-stk[j].x);
				maxd=max(maxd,temp);
			}
			res+=maxd;
		}
	}
	printf("%.0lf\n",res);
	return 0;
}

更新于2021/10/21