BZOJ 1492 [NOI2007]货币兑换Cash(斜率优化dp+splay维护凸壳)

1492: [NOI2007]货币兑换Cash

Time Limit: 5 Sec   Memory Limit: 64 MB
Submit: 5264   Solved: 2142
[ Submit][ Status][ Discuss]

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、B
K、RateK,意义如题目中所述。对于100%的测试数据,满足:0<AK≤10;0<BK≤10;0<RateK≤100;MaxProfit≤1
0^9。
【提示】
1.输入文件可能很大,请采用快速的读入方式。
2.必然存在一种最优的买卖方案满足:
每次买进操作使用完所有的人民币;
每次卖出操作卖出所有的金券。

Output

只有一个实数MaxProfit,表示第N天的操作结束时能够获得的最大的金钱数目。答案保留3位小数。

Sample Input

3 100
1 1 1
1 2 2
2 2 3

Sample Output

225.000

HINT

Source


        中文题,我就不复述题意了……

        首先,由于这个买入和卖出并没有中间商赚取差价(吐槽一下),所以说每一天结束之后的价值既可以用钱来表示也可以用金券来表示。那么这样的话,我们用f[i]表示第i天的时候最多能够得到的钱,x[j]表示第j天的时候把钱换为最多的A的数量,y[j]表示对应的b的数量。那么有转移方程f[i]=max(f[i-1],x[j]*A[i]+y[j]*B[i]),直接枚举复杂度为O(N^2)。显然对于最大可达10^5的数据来说是不够的。

        于是我们很容易去想到斜率优化。同样的套路,假设j>k而且决策j比k更优,那么有x[j]*A[i]+y[j]*B[i]>x[k]*A[i]+y[k]*B[i],化简可以得到(y[j]-y[k])/(x[j]-x[k])<-A[i]/B[i]。如此一来便是一个比较明显的斜率表达式,维护一个单调的上凸壳。但是,很快我们发现,这里并不能想套路的那样做。因为我们这里的x[i]和y[i]并没有什么相关性。因为,这里还涉及到一个比例rate,而这个rate又是完全随机的。没有了单调性,于是就不能简单的用一个单调队列去维护。

        又想起暑假的时候wh学长讲过的,斜率优化遇到这种情况可以用平衡树或者二分去搞,于是开始弄了起来。对于每一个决策点,我都在splay中插入一个点对(x,y)可以对应看作图中的横纵坐标。而splay中的关键字显然就是按照横坐标来确定的。根据斜率优化的特点,最重要的就是要维护这个凸壳。对于每个新插入的点,我都首先在splay中查找,是否有下图所示的节点。图中新加入的A点可以替代C、D中间的两个点,而我们就是要找到这样的两个C、D然后删除中间的点。这里为了避免删除的麻烦,所以我们选择了splay这种平衡树,因为我们可以直接把C、D旋转到A的底下,然后直接从这两点出往下截断即可,省去了多余的删除。

                                                       

        处理了这种情况之后,我们还要考虑,如果A是在原本的凸壳内部,那么要直接把A点删除。对应如下图所示。

                                                                                     

        然后最后结果的话,和普通斜率优化dp一样,在已知-A[i]/B[i]的情况下,找一个最大的满足条件的解即可。最后是关于之前两个步骤如何寻找与确定关系的问题。对于这个我们可以对每一个节点开两个数组lk和rk,分别表示该节点与左边一个节点的斜率和与其右边一个节点的斜率。这样再根据图中的几何关系,我们就可以确定需要的节点。当然这个还要注意处理两端的点的边界问题,还有使用eps的时候也要注意一下。具体见代码:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<cstdlib>
#define INF 0x3f3f3f3f
#define eps 1e-8
#define N 100010

using namespace std;

double f[N],A[N],B[N],lk[N],rk[N],rate[N];
int n,root;

struct SPLAY
{
    int sz,ch[N][2],fa[N];
    double X[N],Y[N];
    bool which(int x){return ch[fa[x]][1]==x;}

    inline void Rotate(int x,int &target)
    {
        int y=fa[x],z=fa[y],wh=which(x);
        if(y!=target)ch[z][ch[z][1]==y]=x;else target=x;
        fa[ch[x][wh^1]]=y;fa[x]=z;fa[y]=x;
        ch[y][wh]=ch[x][wh^1];ch[x][wh^1]=y;
    }
    inline double slope(int x,int y)							//计算两个节点的斜率
    {
        if (fabs(X[y]-X[x])<eps) return -INF;					//一定要用fabs
        return (Y[y]-Y[x])/(X[y]-X[x]);
    }

    inline void splay(int x,int& target)
    {
        while (x!=target)
        {
            int y=fa[x];
            if (y!=target)
            {
                if (which(x)^which(y)) Rotate(x,target);
                                  else Rotate(y,target);
            } Rotate(x,target);
        }
    }

    inline void ins(int &i,double x,double y,int last)
    {
        if (!i)
        {
            X[i=++sz]=x;Y[sz]=y;
            fa[sz]=last; return;
        }
        ins(ch[i][x>X[i]+eps],x,y,i);
    }

    inline int pre(int x)								//往前找左端点
    {
        int i=ch[x][0],res=0;
        while (i)
            if (slope(x,i)+eps>=rk[i]) res=i,i=ch[i][0];		//如果与新加入点的斜率大于原本右边的斜率,那么这个点可以删除
                                  else i=ch[i][1];
        return res;
    }

    inline int nxt(int x)								//往后找右端点
    {
        int i=ch[x][1],res=0;
        while (i)
            if (slope(x,i)<=lk[i]+eps) res=i,i=ch[i][1];		//如果与新加入点的斜率小于原本左边的斜率,那么这个点可以删除
                                  else i=ch[i][0];
        return res;
    }

    inline void del(int x)
    {
        int l=ch[x][0],r=ch[x][1];
        root=ch[x][0]?ch[x][0]:ch[x][1];
        fa[r]=l; ch[l][1]=r; fa[l]=0;
        lk[r]=rk[l]=slope(r,l);
    }

    inline void maintain(int x)
    {
        splay(x,root);
        if (ch[x][0])
        {
            int left=pre(x);								//确定左端点
            if (left)
            {
                splay(left,ch[x][0]);						//直接把左端点旋转成新加入点的左儿子,相当于删除了一段
                ch[left][1]=0; rk[left]=lk[x]=slope(left,x);				//维护lk和rk
            } else lk[x]=-INF;
        } else lk[x]=INF;
        if (ch[x][1])
        {
            int right=nxt(x);								//确定右端点
            if (right)
            {
                splay(right,ch[x][1]);						//直接把右端点旋转成新加入点的右儿子,相当于删除了一段
                ch[right][0]=0; lk[right]=rk[x]=slope(right,x);
            } else rk[x]=INF;
        } else rk[x]=-INF;
        if (lk[x]<=rk[x]+eps) del(x);							//如果在凸壳内部,则直接删除这个点
    }

    int getans(int x,double k)
    {
        while(x)
            if (lk[x]+eps>=k&&k+eps>=rk[x]) return x;						//答案是满足条件的最优解
                          else x=ch[x][lk[x]+eps>k];
        return 0;
    }

} splay;

int main()
{
    splay.sz=0;
    scanf("%d%lf",&n,&f[0]);
    for(int i=1;i<=n;i++)
        scanf("%lf%lf%lf",&A[i],&B[i],&rate[i]);
    for(int i=1;i<=n;i++)
    {
        int j=splay.getans(root,-(A[i]/B[i]));
        double x=splay.X[j],y=splay.Y[j];
        f[i]=max(f[i-1],A[i]*x+B[i]*y);								//dp转移
        y=f[i]/(A[i]*rate[i]+B[i]);
        x=y*rate[i]; splay.ins(root,x,y,0);
        splay.maintain(i);
    }
    printf("%.3lf",f[n]);
}
        然后此题也可以用CDQ分治来处理这个凸壳维护问题,这个下次再补。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值