Description
小Y最近在一家金券交易所工作。该金券交易所只发行交易两种金券:A纪念券(以下简称A券)和 B纪念券(以下简称B券)。每个持有金券的顾客都有一个自己的帐户。金券的数目可以是一个实数。每天随着市场的起伏波动,两种金券都有自己当时的价值,即每一单位金券当天可以兑换的人民币数目。我们记录第 K 天中 A券 和 B券 的价值分别为 AK 和 BK(元/单位金券)。为了方便顾客,金券交易所提供了一种非常方便的交易方式:比例交易法。
比例交易法分为两个方面:
(a)卖出金券:顾客提供一个 [0,100] 内的实数 OP 作为卖出比例,其意义为:将 OP% 的 A券和 OP% 的 B券 以当时的价值兑换为人民币;
(b)买入金券:顾客支付 IP 元人民币,交易所将会兑换给用户总价值为 IP 的金券,并且,满足提供给顾客的A券和B券的比例在第 K 天恰好为 RateK;例如,假定接下来 3 天内的 Ak、Bk、RateK 的变化分别为:
假定在第一天时,用户手中有 100元 人民币但是没有任何金券。用户可以执行以下的操作:
注意到,同一天内可以进行多次操作。小Y是一个很有经济头脑的员工,通过较长时间的运作和行情测算,他已经知道了未来N天内的A券和B券的价值以及Rate。他还希望能够计算出来,如果开始时拥有S元钱,那么N天后最多能够获得多少元钱。
Input
输入第一行两个正整数N、S,分别表示小Y能预知的天数以及初始时拥有的钱数。接下来N行,第K行三个实数AK、BK、RateK,意义如题目中所述。
对于100%的测试数据,满足:0<AK≤10;0<BK≤10;0<RateK≤100;MaxProfit≤10^9。
【提示】
输入文件可能很大,请采用快速的读入方式。
必然存在一种最优的买卖方案满足:
每次买进操作使用完所有的人民币;
每次卖出操作卖出所有的金券。
Output
只有一个实数MaxProfit,表示第N天的操作结束时能够获得的最大的金钱数目。答案保留3位小数。
题目分析
注意到题面里面有一个十分有启发意义的提示
每次买进操作使用完所有的人民币;
每次卖出操作卖出所有的金券
设
d
p
[
i
]
dp[i]
dp[i]为第
i
i
i天最多能有多少钱
根据上面的提示,我们知道要使第
i
i
i天的前尽量多
应该在第
j
j
j天花光所有钱买入,再在第
i
i
i天全部卖出,那么可以得到转移方程
d
p
[
i
]
=
m
a
x
(
d
p
[
i
−
1
]
,
d
p
[
j
]
∗
R
j
R
j
∗
A
j
+
B
j
∗
A
i
+
d
p
[
j
]
R
j
∗
A
j
+
B
j
∗
B
i
)
,
j
∈
[
1
,
i
)
dp[i]=max(dp[i-1],\frac{dp[j]*R_j}{R_j*A_j+B_j}*A_i+\frac{dp[j]}{R_j*A_j+B_j}*B_i),j\in[1,i)
dp[i]=max(dp[i−1],Rj∗Aj+Bjdp[j]∗Rj∗Ai+Rj∗Aj+Bjdp[j]∗Bi),j∈[1,i)
其中
d
p
[
i
−
1
]
dp[i-1]
dp[i−1]的转移表示第
i
i
i天什么都不做
为方便表示,记
X
i
=
d
p
[
i
]
∗
R
i
R
i
∗
A
i
+
B
i
,
Y
i
=
d
p
[
i
]
R
i
∗
A
i
+
B
i
X_i=\frac{dp[i]*R_i}{R_i*A_i+B_i},Y_i=\frac{dp[i]}{R_i*A_i+B_i}
Xi=Ri∗Ai+Bidp[i]∗Ri,Yi=Ri∗Ai+Bidp[i]
将上述转移方程
d
p
[
i
]
=
X
j
∗
A
[
i
]
+
Y
j
∗
B
[
i
]
dp[i]=X_j*A[i]+Y_j*B[i]
dp[i]=Xj∗A[i]+Yj∗B[i]变形得
Y
j
=
−
A
i
B
i
X
j
+
d
p
[
i
]
B
[
i
]
Y_j=-\frac{A_i}{B_i}X_j+\frac{dp[i]}{B[i]}
Yj=−BiAiXj+B[i]dp[i]
这个式子显然就是要我们斜率优化啊,也就是要维护一个上凸壳
但是
X
i
X_i
Xi不单调,斜率
k
=
−
A
i
B
i
k=-\frac{A_i}{B_i}
k=−BiAi也不单调,普通的单调队列不能维护
所以用动态性更强的平衡树来维护,一般选择Splay
或者用CDQ分治离线处理也可以
Splay维护凸包
splay内的结点按
X
i
X_i
Xi的大小 维护
并维护每个结点与左/右相邻结点的斜率
l
k
[
i
]
,
r
k
[
i
]
lk[i],rk[i]
lk[i],rk[i],及时删除不满足上凸壳的点
每次对于一条斜率为
K
=
−
A
i
B
i
K=-\frac{A_i}{B_i}
K=−BiAi的查询,查找一个满足
l
k
[
j
]
>
=
K
lk[j]>=K
lk[j]>=K且
K
<
=
r
k
[
j
]
K<=rk[j]
K<=rk[j]的点
j
j
j来更新
d
p
[
i
]
dp[i]
dp[i]
//splay维护凸包
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<queue>
#include<map>
using namespace std;
typedef long long lt;
typedef double dd;
#define eps 1e-9
int read()
{
int f=1,x=0;
char ss=getchar();
while(ss<'0'||ss>'9'){if(ss=='-')f=-1;ss=getchar();}
while(ss>='0'&&ss<='9'){x=x*10+ss-'0';ss=getchar();}
return f*x;
}
const dd inf=1e9;
const int maxn=200010;
int n,m,rt;
int fa[maxn],ch[maxn][2];
dd lk[maxn],rk[maxn];
dd A[maxn],B[maxn],R[maxn];
dd X[maxn],Y[maxn];
dd dp[maxn];
void rotate(int& p,int x)
{
int y=fa[x],z=fa[y];
int d=(ch[y][0]==x);
if(y==p) p=x;
else if(ch[z][0]==y) ch[z][0]=x;
else ch[z][1]=x;
fa[y]=x; fa[ch[x][d]]=y; fa[x]=z;
ch[y][d^1]=ch[x][d]; ch[x][d]=y;
}
void splay(int& p,int x)
{
while(x!=p)
{
int y=fa[x],z=fa[y];
if(y!=p)
{
if((ch[y][0]==x)^(ch[z][0]==y)) rotate(p,x);
else rotate(p,y);
}
rotate(p,x);
}
}
dd calc(int j1,int j2)//计算斜率
{
if(X[j1]-X[j2]<eps&&X[j1]-X[j2]>-eps) return -inf;
else return (Y[j2]-Y[j1])/(X[j2]-X[j1]);
}
int pre(int x)
{
int u=ch[x][0],res=u;
while(u)
{
if(lk[u]+eps>=calc(u,x)) res=u,u=ch[u][1];
else u=ch[u][0];
}
return res;
}
int nxt(int x)
{
int u=ch[x][1],res=u;
while(u)
{
if(rk[u]<=calc(x,u)+eps) res=u,u=ch[u][0];
else u=ch[u][1];
}
return res;
}
int find(int x,dd K)
{
if(!x) return 0;
if(lk[x]>=K+eps&&rk[x]<=K+eps) return x;
else if(lk[x]<K+eps) return find(ch[x][0],K);
else return find(ch[x][1],K);
}
void update(int x)
{
splay(rt,x);
if(ch[x][0])
{
int lc=pre(x);//找到左边最后一个可以与x构成上凸壳的点
splay(ch[x][0],lc); ch[lc][1]=0;
lk[x]=rk[lc]=calc(lc,x);
}
else lk[x]=inf;
if(ch[x][1])
{
int rc=nxt(x);//找到右边第一个可以与x构成上凸壳的点
splay(ch[x][1],rc); ch[rc][0]=0;
lk[rc]=rk[x]=calc(x,rc);
}
else rk[x]=-inf;
if(lk[x]<=rk[x]+eps)//新加入的点不能与原来的点构成上凸壳,所以删除
{
rt=ch[x][0]; ch[rt][1]=ch[x][1];
fa[ch[x][1]]=rt; fa[rt]=0;
rk[rt]=lk[ch[rt][1]]=calc(rt,ch[rt][1]);
}
}
void ins(int &x,int pa,int u)
{
if(!x){ x=u; fa[x]=pa; return;}
if(X[u]<=X[x]+eps) ins(ch[x][0],x,u);//splay按x=Pi大小排序
else ins(ch[x][1],x,u);
}
int main()
{
scanf("%d%lf",&n,&dp[0]);
for(int i=1;i<=n;++i)
{
scanf("%lf%lf%lf",&A[i],&B[i],&R[i]);
int j=find(rt,-A[i]/B[i]);//找最优更新点
dp[i]=max(dp[i-1],A[i]*X[j]+B[i]*Y[j]);
Y[i]=dp[i]/(R[i]*A[i]+B[i]); X[i]=Y[i]*R[i];
ins(rt,0,i); update(i);//新的点插入splay维护凸包
}
printf("%.3lf",dp[n]);
return 0;
}
CDQ分治维护凸包
先把每次需要查询的斜率按大小排序
CDQ分治处理区间
[
l
l
,
r
r
]
[ll,rr]
[ll,rr]时,先利用归并按询问时间为第二关键字对该区间排序
先向下递归处理左子区间
处理完后左子区间后构造左子区间所有点构成的上凸壳
利用该上凸壳处理右子区间的询问
由于斜率、询问时间有序,所以可以直接用栈维护
处理完右子区间的询问后递归处理右子区间
两个子区间处理完毕后对 区间
[
l
l
,
r
r
]
[ll,rr]
[ll,rr]按
X
i
X_i
Xi排序,以便父区间构造凸壳
//CDQ维护凸包
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<queue>
#include<map>
using namespace std;
typedef long long lt;
typedef double dd;
#define eps 1e-9
int read()
{
int f=1,x=0;
char ss=getchar();
while(ss<'0'||ss>'9'){if(ss=='-')f=-1;ss=getchar();}
while(ss>='0'&&ss<='9'){x=x*10+ss-'0';ss=getchar();}
return f*x;
}
const dd inf=1e9;
const int maxn=200010;
int n;
struct node{dd A,B,k,R,X,Y;int id;}Q[maxn],tt[maxn];
bool cmp(node a,node b){return a.k<b.k;}
dd dp[maxn];
int st[maxn];
dd calc(int j1,int j2)
{
if(fabs(Q[j1].X-Q[j2].X)<eps) return -inf;
else return (Q[j2].Y-Q[j1].Y)/(Q[j2].X-Q[j1].X);
}
void merge(int ll,int rr)
{
int mid=ll+rr>>1,t1=ll,t2=mid+1,p=ll;
while(t2<=rr)
{
while(Q[t1].X<=Q[t2].X&&t1<=mid) tt[p++]=Q[t1++];
tt[p++]=Q[t2++];
}
while(t1<=mid) tt[p++]=Q[t1++];
while(t2<=rr) tt[p++]=Q[t2++];
for(int i=ll;i<=rr;++i) Q[i]=tt[i];
}
void CDQ(int ll,int rr)
{
if(ll==rr)//此时ll之前的询问都处理完毕,可以更新dp[ll]
{
dp[ll]=max(dp[ll],dp[ll-1]);
Q[ll].Y=dp[ll]/(Q[ll].A*Q[ll].R+Q[ll].B);
Q[ll].X=Q[ll].Y*Q[ll].R;
return;
}
int top=0;
int mid=ll+rr>>1,t1=ll,t2=mid+1;
for(int i=ll;i<=rr;++i)//按询问时间顺序排序
if(Q[i].id<=mid) tt[t1++]=Q[i];
else tt[t2++]=Q[i];
for(int i=ll;i<=rr;++i) Q[i]=tt[i];
CDQ(ll,mid);//处理左子区间
for(int i=ll;i<=mid;++i)//左子区间Xi有序,构造左子区间的点构成的凸壳
{
while(top>=2&&calc(st[top-1],st[top])<calc(st[top],i)+eps) top--;
st[++top]=i;
}
for(int i=mid+1;i<=rr;++i)//利用左子区间更新右子区间
{
while(top>=2&&calc(st[top-1],st[top])<=Q[i].k+eps) --top;//斜率有序保证了栈的可行性
int j=st[top];
dp[Q[i].id]=max(dp[Q[i].id],Q[j].X*Q[i].A+Q[j].Y*Q[i].B);
}
CDQ(mid+1,rr); merge(ll,rr);//递归处理右子区间后,对[ll,rr]按Xi排序
}
int main()
{
scanf("%d%lf",&n,&dp[0]);
for(int i=1;i<=n;++i)
{
scanf("%lf%lf%lf",&Q[i].A,&Q[i].B,&Q[i].R);
Q[i].k=-Q[i].A/Q[i].B; Q[i].id=i;
}
sort(Q+1,Q+1+n,cmp); CDQ(1,n);//先按每次询问斜率排序
printf("%.3lf",dp[n]);
return 0;
}