BZOJ1492 || 洛谷P4027 [NOI2007]货币兑换【斜率优化】【Splay/CDQ分治维护凸包】

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[i1],RjAj+Bjdp[j]RjAi+RjAj+Bjdp[j]Bi),j[1,i)
其中 d p [ i − 1 ] dp[i-1] dp[i1]的转移表示第 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=RiAi+Bidp[i]Ri,Yi=RiAi+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]=XjA[i]+YjB[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 ] &gt; = K lk[j]&gt;=K lk[j]>=K K &lt; = r k [ j ] K&lt;=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;
}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值