6700. 【2020.06.07省选模拟】得分

题目

n n n个物品,价值为 A i A_i Ai。取得一个物品需要花费 t i t_i ti的时间。
T = ∑ t i T=\sum t_i T=ti。这些物品都要取完,总共要花 T T T的时间。
物品的价值会随着时间递增而减少。
如果在时刻 x x x取完物品 i i i,则此时收获的价值为 A i ( 1 − c x T ) A_i(1-\frac{cx}{T}) Ai(1Tcx)
c c c的最大值,使得:对于所有的最终获得价值尽量多的最优方案,都不存在满足 A i > A j A_i>A_j Ai>Aj i , j i,j i,j同时满足 A i ( 1 − c x i T ) < A j ( 1 − c x j T ) A_i(1-\frac{cx_i}{T})<A_j(1-\frac{cx_j}{T}) Ai(1Tcxi)<Aj(1Tcxj)
n ≤ 2 e 5 n\leq2e5 n2e5


思考历程&正解

看到部分分中的 A i t i \frac{A_i}{t_i} tiAi,心中有些想法。知道自己不会证于是写了程序拍一下发现是对的……
(具体证明可以考虑相邻物品的顺序)
接下来的问题是若何决定 A i t i \frac{A_i}{t_i} tiAi相同的块中内部的顺序。
于是接下来另外猜结论,证明靠拍,搞了半天发现下一个结论是错了……
后来发现我思想僵化,以为一定存在一种排列方式,使得它的 c c c永远是最大的……

在乱搞的时候很容易发现这条式子:
T c ≥ A i x i − A j x j A i − A j \frac{T}{c}\geq\frac{A_ix_i-A_jx_j}{A_i-A_j} cTAiAjAixiAjxj
右边的那条东西显然是个斜率的形式。
那么可以考虑,对于 i i i来说,最大的斜率是多少。
那么我们希望 x i x_i xi尽量大, x j x_j xj尽量小。
于是钦定 i i i所在块最后一个被选的(时间记为 R i R_i Ri), j j j为所在块第一个被选的(时间记为 L i L_i Li)。这个时间可以预处理出来。
按照 A i A_i Ai从小往大的顺序操作。建立平面直角坐标系。在加入 A i A_i Ai之前,计算当前坐标系中的点和 ( A i , A i R i ) (A_i,A_iR_i) (Ai,AiRi)的最大斜率;计算之后将 ( A i , A i L i ) (A_i,A_iL_i) (Ai,AiLi)丢入坐标系中。
比赛时我二分 1 c \frac{1}{c} c1的值,将其转化为判定性问题之后,发现特别好写……
如果我不是被垃圾C++坑了,我就AC了……(二分的精度设置了1e-10,然后到了这个精度之后 m i d mid mid算出来永远跟 l l l一样,然后就死循环了……)

比赛之后gmh77发表了他的 O ( n ) O(n) O(n)的撵爆正解的方法:
不需要二分,只需要比较相邻点即可……
具体的理解,可以自己画图看看,或者根据二分之后判定的代码,不难发现这个是等价的。


代码

using namespace std;
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <climits>
#define N 200010
#define ll long long
int n;
struct Problem{
	int a,t;
} p[N];
bool cmpp(Problem x,Problem y){return (ll)x.a*y.t>(ll)y.a*x.t;}
double L[N],R[N];
int q[N];
bool cmpq(int x,int y){return p[x].a<p[y].a;}
bool judge(double k){
	double b=1e17,lstx=0;
	for (int i=1;i<=n;++i){
		double x=p[q[i]].a,y=p[q[i]].a*R[q[i]];
		b+=(x-lstx)*k;
		if (lstx<x && b<y)
			return 0;
		y=p[q[i]].a*L[q[i]];
		b=min(b,y);
		lstx=x;
	}
	return 1;
}
int main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	freopen("score.in","r",stdin);
	freopen("score.out","w",stdout);
	scanf("%d",&n);
	for (int i=1;i<=n;++i)
		scanf("%d",&p[i].a);
	ll T=0;
	for (int i=1;i<=n;++i)
		scanf("%d",&p[i].t),T+=p[i].t;
	sort(p+1,p+n+1,cmpp);
	p[0]={1,0},p[n+1]={0,1};
	ll sumt=0;
	for (int i=1;i<=n;++i){
		L[i]=((ll)p[i].a*p[i-1].t==(ll)p[i-1].a*p[i].t?L[i-1]:sumt);
		sumt+=p[i].t;
	}
	for (int i=1;i<=n;++i)
		L[i]+=p[i].t;
	for (int i=n;i>=1;--i){
		R[i]=((ll)p[i].a*p[i+1].t==(ll)p[i+1].a*p[i].t?R[i+1]:sumt);
		sumt-=p[i].t;
	}
	for (int i=1;i<=n;++i)
		L[i]/=T,R[i]/=T;
	for (int i=1;i<=n;++i)
		q[i]=i;
	sort(q+1,q+n+1,cmpq);
	double l=0,r=1e16;
	while (r-l>1e-8){
		double mid=(l+r)/2;
		if (judge(mid))
			r=mid;
		else
			l=mid;
	}
	double c=1/r;
	c=min(c,1.0);
	printf("%.9lf\n",c);
	return 0;
}

总结

二分精度不能太大,学到了……

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值