洛谷-4027 【NOI2007】 货币兑换

题目描述

小 Y 最近在一家金券交易所工作。该金券交易所只发行交易两种金券:A 纪念券(以下简称 A 券)和 B 纪念券(以下简称 B 券)。每个持有金券的顾客都有一个自己的帐户。金券的数目可以是一个实数。

每天随着市场的起伏波动,两种金券都有自己当时的价值,即每一单位金券当天可以兑换的人民币数目。我们记录第 K 天中 A 券和 B 券的价值分别为 AKA_KAK​ 和BKB_KBK​ (元/单位金券)。

为了方便顾客,金券交易所提供了一种非常方便的交易方式:比例交易法。

比例交易法分为两个方面:

a) 卖出金券:顾客提供一个[0,100]内的实数 OP 作为卖出比例,其意义为:将 OP%的 A 券和 OP%的 B 券以当时的价值兑换为人民币;

b) 买入金券:顾客支付 IP 元人民币,交易所将会兑换给用户总价值为IP 的金券,并且,满足提供给顾客的 A 券和 B 券的比例在第 K 天恰好为 RateKRate_KRateK​ ;

例如,假定接下来 3 天内的 AkA_kAk​ 、BkB_kBk​ 、RateKRate_KRateK​ 的变化分别为

时间 AkA_kAk​ BkB_kBk​ RatekRate_kRatek​

第一天 1 1 1

第二天 1 2 2

第三天 2 2 3

假定在第一天时,用户手中有 100 元人民币但是没有任何金券。

用户可以执行以下的操作:

时间 用户操作 人民币(元) A 券的数量 B 券的数量

开户 无 100 0 0

第一天 买入 100 元 0 50 50

第二天 卖出 50% 75 25 25

第二天 买入 60 元 15 55 40

第三天 卖出 100% 205 0 0

注意到,同一天内可以进行多次操作。

小 Y 是一个很有经济头脑的员工,通过较长时间的运作和行情测算,他已经知道了未来 N 天内的 A 券和 B 券的价值以及 Rate。他还希望能够计算出来,如果开始时拥有 S 元钱,那么 N 天后最多能够获得多少元钱。

输入输出格式

输入格式:

第一行两个正整数 N、S,分别表示小 Y 能预知的天数以及初始时拥有的钱数。

接下来 N 行,第 K 行三个实数 AKA_KAK​ 、BKB_KBK​ 、RateKRate_KRateK​ ,意义如题目中所述。

输出格式:

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

输入输出样例

输入样例#1:

3 100
1 1 1
1 2 2
2 2 3

输出样例#1:

225.000

说明

时间 用户操作 人民币(元) A 券的数量 B 券的数量

开户 无 100 0 0

第一天 买入 100 元 0 50 50

第二天 卖出 100% 150 0 0

第二天 买入 150 元 0 75 37.5

第三天 卖出 100% 225 0 0

本题没有部分分,你的程序的输出只有和标准答案相差不超过0.001 时,才能获得该测试点的满分,否则不得分。

测试数据设计使得精度误差不会超过 107 10 − 7

对于 40%的测试数据,满足 N ≤ 10;

对于 60%的测试数据,满足 N ≤ 1 000;

对于 100%的测试数据,满足 N ≤ 100 000;

对于 100%的测试数据,满足:

0 < AK A K ​ ≤ 10;

0 < BK B K ​ ≤ 10;

0 < RateK R a t e K ≤ 100

MaxProfit M a x P r o f i t ≤ 10910^9109 ;

输入文件可能很大,请采用快速的读入方式。

必然存在一种最优的买卖方案满足:

每次买进操作使用完所有的人民币;

每次卖出操作卖出所有的金券。

说出来你可能不信,我一开始以为是买股票,只能整数整数地买卖

题解

认真做的话应该是平衡树维护凸包,但是我完全搞不懂(一会可能还要去看看)
于是就按照CDQ论文来做了一下

简单来讲:CDQ+斜率优化

不用考虑哪一天的钱买哪一天的货币,只要考虑兑换之后的货币在哪一天换掉即可(这是句废话,逻辑混乱并且完全不需要读懂)

考虑第 i i 天卖出货币(按照买股票的说法来讲)
那么,值得一提的就是,获得货币的时间只能是 [1i1] [ 1 , i − 1 ] 之间
假设第 i i 天在手里的货币量为 X[i],Y[i] X [ i ] , Y [ i ] ,最大收益为 f[i] f [ i ]
货币兑换为 CA[i],CB[i] C A [ i ] , C B [ i ]

那么第 i i 天兑换货币的量为
CA[i]=f[i]/(AiRate[i]+Bi)Rate[i] C A [ i ] = f [ i ] / ( A i ∗ R a t e [ i ] + B i ) ∗ R a t e [ i ]
CB[i]=f[i]/(AiRate[i]+Bi) C B [ i ] = f [ i ] / ( A i ∗ R a t e [ i ] + B i )

那么在第 i i 天把货币都换成钱就有点麻烦了(货币好像就是钱??)
那么对于 j,k[1,i1] j , k ∈ [ 1 , i − 1 ] , j j 优于 k k 就有

CA[j]Ai+CB[j]Bi>CA[k]Ai+CB[k]Bi C A [ j ] ∗ A i + C B [ j ] ∗ B i > C A [ k ] ∗ A i + C B [ k ] ∗ B i

(CA[j]CA[k])(Ai)<(CB[j]CB[k])Bi ( C A [ j ] − C A [ k ] ) ∗ ( − A i ) < ( C B [ j ] − C B [ k ] ) ∗ B i

因为 j,k j , k 无序(我们没有规定它有序)
因此不妨设 CA[j]>CA[k] C A [ j ] > C A [ k ]

ki=AiBi<CB[j]CB[k]CA[j]CA[k] k i = − A i B i < C B [ j ] − C B [ k ] C A [ j ] − C A [ k ]

注意!!此处 j,k j , k 无序,只有 j,k[1,i1] j , k ∈ [ 1 , i − 1 ]

那么根据这个,我们就可以将每一个点的 CB C B 作为 x x CA C A 作为 y y
然后按照 x,y x , y 升序排序,然后维护一个凸包

于是乎,问题解决了一半
另一半的问题是

j,k j , k 无序,那我拿头去维护这个凸包??(可能拿头维护都维护不了)

换句话说 当有一个新的点 i i 被处理完,并放进来的时候,我们并不知道 i i 的插入位置
正规做法用Splay解决了这个问题,但是我们选择用更加简单粗暴的方法:CDQ分治
仔细回想一下 CDQ C D Q 的处理过程你就会发现,它始终是用前面的更新后面的

所以 解法如下

当前处理区间 [l,r] [ l , r ]
①先处理 [l,mid] [ l , m i d ]
②然后将 [l,mid] [ l , m i d ] 内的点按照 x,y, x , y , 升序排列,维护一个凸包
③然后用这个凸包更新 [mid+1,r] [ m i d + 1 , r ] 内的点的 f f ,并且要求这些点的 k k 按照降序排序(免去二分查找的复杂度,可以扫一遍直接过)
④再处理 [mid+1,r] [ m i d + 1 , r ]
⑤重新按照原本的顺序排序

注意一个细节

对于当前的单个点 i i ,它的最优 f f 不一定是在它当天卖掉所有货币,而是继承昨天的决策(啃老是可以的)

附上略有一点参考价值的伪代码

#include 
#include 
#include 
using namespace std;
const double eps=1e-9;
const double inf=1e9;

inline double fabs(double x){return x>0?x:-x;}

struct node
{
    double q,a,b,r,k;
    int i;
}A[100123],B[100123];
bool cmp(node a,node b){return a.kstruct pot{double x,y;}p[100123],np[100123];
bool operator < (pot a,pot b){return (a.xfabs(a.x-b.x)<=eps)&&(a.ydouble f[100123];
int st[100123];

double slop(int a,int b)
{
    if(fabs(p[a].x-p[b].x)<=eps)return -inf;
    return (p[b].y-p[a].y)/(p[b].x-p[a].x);
}

void solve(int l,int r)
{
    if(l==r)
    {
        f[l]=max(f[l],f[l-1]);//最优决策可能是继承昨天的
        p[l].y=f[l]/(A[l].a*A[l].r+A[l].b);
        p[l].x=p[l].y*A[l].r;
        return;
    }
    int mid=l+r>>1,l1=l,l2=mid+1;
    for(int i=l;i<=r;i++)
        if(A[i].i<=mid)B[l1++]=A[i];
        else B[l2++]=A[i];//按照正常顺序分左右区间,但此时左右区间的点仍然分别按照k的降序排序(分别)
    for(int i=l;i<=r;i++)A[i]=B[i];
    solve(l,mid);//处理左区间
    int top=0;
    for(int i=l;i<=mid;i++)
    {
        while(top>1&&slop(i,st[top])+eps>slop(st[top],st[top-1]))top--;
        st[++top]=i;
    }//维护凸包
    int j=1;
    for(int i=r;i>mid;i--)
    {
        while(j1])+eps)j++;//因为k按照降序排序,所以可以直接枚举斜率从小到大
        f[A[i].i]=max(f[A[i].i],p[st[j]].x*A[i].a+p[st[j]].y*A[i].b);//更新答案
    }
    solve(mid+1,r);//处理右区间
    l1=l;l2=mid+1;
    //归并排序,按照斜率的点进行排序
    for(int i=l;i<=r;i++)
        if((p[l1]r)&&l1<=mid)np[i]=p[l1++];
        else np[i]=p[l2++];
    for(int i=l;i<=r;i++)p[i]=np[i];
}

int main()
{
    freopen("In.txt","r",stdin);
    int n;
    scanf("%d%lf",&n,&f[0]);
    for(int i=1;i<=n;i++)
    {
        A[i].i=i;
        scanf("%lf%lf%lf",&A[i].a,&A[i].b,&A[i].r);
        A[i].k=-A[i].a/A[i].b;
    }
    sort(A+1,A+n+1,cmp);//按照k的降序排序
    solve(1,n);
    printf("%.3lf\n",f[n]);
    return 0;
}

你可能感兴趣的:(NOI,分治,斜率优化,动态规划)