【NOI2007】【BZOJ1492】货币兑换Cash

BZOJ题面是图片来的.

文字版题面

1797 货币兑换

2007年NOI全国竞赛

时间限制: 1 s

空间限制: 128000 KB

题目等级 : 大师 Master

题目描述 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的变化分别为:

时间 Ak Bk Ratek

第一天 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 天后最多能够获得多少元钱。

输入描述 Input Description

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

接下来 N 行,第 K 行三个实数 AK、BK、RateK,意义如题目中所述。

输出描述 Output Description

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

样例输入 Sample Input

3 100

1 1 1

1 2 2

2 2 3

样例输出 Sample Output

225.000

数据范围及提示 Data Size & Hint

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

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

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

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

对于 100%的测试数据,满足: 0 < AK ≤ 10; 0 < BK ≤ 10; 0 < RateK ≤ 100;MaxProfit ≤ 109;

样例说明

开户 无 100 0 0 第一天 买入 100 元 0 50 50 第二天 卖出 100% 150 0 0 第二天 买入 150 元 0 75 37.5 第三天 卖出 100% 225 0 0

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

开户 无 100 0 0

第一天 买入100元 0 50 50

第二天 卖出100% 150 0 0

第二天 买入150元 0 75 37.5

第三天 卖出100% 225 0 0

这只是个简单的cdq…真的…

然而斜率优化DP我还没有学过QAQ

这道题既练了cdq又学了斜率优化DP>ω<

在浮点数判断的时候无脑在两边加个1e-9导致某个点死活过不去

最后乱搞删掉了竟然过了卧槽(╯‵□′)╯︵┻━┻

速度倒不是很快甚至比某些Splay维护的还要慢…

不过毕竟好写而且A了不是吗.

(对着论文YY代码真SXBK)

先是暴力代码O(n2)很好写大家都会

#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
#define MAXN 100100
using namespace std;
int n,s;
double a[MAXN],b[MAXN],rate[MAXN];
double ans;
double f[MAXN];
int main()
{
    scanf("%d%d",&n,&s);
    for (int i=1;i<=n;i++) scanf("%lf%lf%lf",&a[i],&b[i],&rate[i]);
    ans=s;
    f[1]=s*rate[1]/(a[1]*rate[1]+b[1]);
    for (int i=2;i<=n;i++)
    {
        for (int j=1;j<i;j++)
        {
            double x=f[j]*a[i]+f[j]/rate[j]*b[i];
            ans=max(ans,x);
        }
        f[i]=ans*rate[i]/(a[i]*rate[i]+b[i]);
    }
    printf("%.3f",ans);
}

然后是AC做法(其实我是从论文看的要不我写个毛斜率优化):

由之前的DP方程:

如果在第i天,有两个决策j,k供我们选择,若决策j比决策k要优,那么有

(f[j]?f[k])?A[i]+(f[j]/Rate[j]?f[k]/Rate[k])?B[i]>0

其中f和暴力里意义一样,是第j天A最多数目

为了方便,我们设f[j]<f[k],g[j]=f[j]/Rate[j],g[k]=f[k]/Rate[k]也就是说g为某天与f对应的B的数目

那么原式就可以化简成斜率形式了:

(g[j]?g[k])/(f[j]?f[k])<?A[i]B[i]

这样看的话不就可以用斜率优化了吗~

所以2007年的NOI考场上,神犇们都开始动手码一棵棵平衡树来维护凸包斜率了>0<

还给不给蒟蒻+手残一点活路啦喂!

还好新世纪的我们有了cdq分治www

于是我们可以把每一天当成一个操作,以?A[i]B[i]为关键字构成操作序列然后cdq分治走起.

利用已经完成的操作序列去更新后面的答案.

既然已经知道斜率优化和cdq了,怎么写就很好办啦~我直接贴代码好了

//AC code by CreationAugust
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
#define MAXN 100100
#define MAXDBL 1e19+9
#define delta 1e-9
using namespace std;
struct Query
{
    int id;
    double a,b,rate;//a为当天A价格,b为当天B价格,rate为当天比例
    double x,y;//维护点(x,y) 即(f[i],g[i]) f[i]是A券最多数目,g[i]是对应B券数目
    double k;//价格构成的斜率,也是cdq排序的关键字
    bool operator <(const Query& a)const{
        return k>a.k;
    }
}ques[MAXN],newq[MAXN];
double ans[MAXN];//ans是最优答案
int stack[MAXN];//建栈维护一个凸包的凸线
int n,s;
double slope(int a,int b)//a,b两点间斜率
{
    if (!b) return -MAXDBL;
    if (fabs(ques[a].x-ques[b].x)<delta) return MAXDBL;
    return (ques[b].y-ques[a].y)/(ques[b].x-ques[a].x);
}
void solve(int l,int r)
{
    int mid=(l+r)>>1,tp1=l,tp2=mid+1;
    if (l==r)
    {
        ans[l]=max(ans[l],ans[l-1]);
        ques[l].y=ans[l]/(ques[l].a*ques[l].rate+ques[l].b);
        ques[l].x=ques[l].y*ques[l].rate;
        return;
    }
    for (int i=l;i<=r;i++)//按时间划分操作序列
        if (ques[i].id<=mid) newq[tp1++]=ques[i];
        else newq[tp2++]=ques[i];
    memcpy(ques+l,newq+l,sizeof(Query)*(r-l+1));
    solve(l,mid);
    int top=0,j=1;
    for (int i=l;i<=mid;i++)
    {
        while (top>1&&(slope(stack[top-1],stack[top])-slope(stack[top-1],i)<delta)) top--;
        stack[++top]=i;
    }
    stack[++top]=0;//注意要设定边界
    for (int i=mid+1;i<=r;i++)
    {
        while (j<top&&ques[i].k-slope(stack[j],stack[j+1])<delta) j++;
        ans[ques[i].id]=max(ans[ques[i].id],ques[stack[j]].x*ques[i].a+ques[stack[j]].y*ques[i].b);
    }
    solve(mid+1,r);
    tp1=l,tp2=mid+1;
    for (int i=l;i<=r;i++)
        if ((((ques[tp1].x<ques[tp2].x)||(fabs(ques[tp1].x-ques[tp2].x)<delta&&ques[tp1].y<ques[tp2].y))||tp2>r)&&tp1<=mid) newq[i]=ques[tp1++];
        else newq[i]=ques[tp2++];
    memcpy(ques+l,newq+l,sizeof(Query)*(r-l+1));
}
int main()
{
    scanf("%d%d",&n,&s);
    ans[0]=s;
    for (int i=1;i<=n;i++)
    {
        scanf("%lf%lf%lf",&ques[i].a,&ques[i].b,&ques[i].rate);
        ques[i].id=i;ques[i].k=-ques[i].a/ques[i].b;
    }
    sort(ques+1,ques+n+1);
    solve(1,n);
    printf("%.3f",ans[n]);
}

代码不长,比平衡树和谐多了,跑的也不算很慢(虽然比某些神犇的Splay跑的还慢QAQ)

时间: 2024-10-13 22:27:30

【NOI2007】【BZOJ1492】货币兑换Cash的相关文章

[NOI2007 Day1] 货币兑换 Cash

vijos P1508 / BZOJ 1492 膜拜了这么久的cdq分治,终于有机会亲自来写了.虽然这个思想很好理解,先做前一半,计算前一半对后一半的影响,再做后一半.但是由于我这个傻Ⅹ,以前既没有做过斜率优化,也没有做过维护凸包之类,花了好久时间捣鼓具体做法,而且理解思路后写起来还是有点难度的. 主要网上的解题各有各的思路,有的是F数组存最多多少B券,有的是存最多多少A券,虽然大同小异,但是一开始我没意识到所以orz了. 参考资料: <从Cash谈一类分治算法的应用>——cdq <cd

bzoj-1492 货币兑换Cash (1)——平衡树维护凸包

题意: 有n天和m的初始金钱,用来购买AB两种纪念券: n天里每天都有AB的价格.每天能够进行这种操作. 1.卖出手中x%的纪念券(AB分别都卖出x%). 2.用x的金钱买入纪念券.买入AB券的比例在第i天为Rate i: 求n天过去之后所获得的最大收益. 金钱和券数均为实数: n<=100 000: 题解: 首先,尽管题中的买入和卖出都是随意数量的.可是相同的纪念券,分几天卖出得到的收 益.一定小于等于直接在一天卖出的收益: 相同.分几天买入也是不如一天花全部钱买入的: 令: f[i]为第i天

bzoj-1492 货币兑换Cash (2)——CDQ分治

题意: 略 见上一篇 题解: 方程还是那个方程f[i]=A[i] * X[j] + B[i] * Y[j]: 化简为Y[i]=(-A[i]/B[i]) * X[i] + f[i]/B[i]这一坨: 既然这个斜率不单调,那排个序让它单调不就行了: 排序之后的问题就是,在i前面更新i的点不一定可以更新i,而应该用来更新i的点说不定还在i的后面: 那么这时候就是用CDQ分治解决: 经典的四步先贴上来: 1.将操作按照时间划分为两个子区间: 2.递归处理左区间的修改与询问: 3.用左区间的修改处理右区间

[BZOJ1492][NOI2007]货币兑换Cash(斜率优化+CDQ分治)

1492: [NOI2007]货币兑换Cash Time Limit: 5 Sec  Memory Limit: 64 MBSubmit: 5838  Solved: 2345[Submit][Status][Discuss] Description 小Y最近在一家金券交易所工作.该金券交易所只发行交易两种金券:A纪念券(以下简称A券)和 B纪念券(以下 简称B券).每个持有金券的顾客都有一个自己的帐户.金券的数目可以是一个实数.每天随着市场的起伏波动, 两种金券都有自己当时的价值,即每一单位金

【BZOJ1492】[NOI2007]货币兑换Cash 斜率优化+cdq分治

[BZOJ10492][NOI2007]货币兑换Cash Description 小Y最近在一家金券交易所工作.该金券交易所只发行交易两种金券:A纪念券(以下简称A券)和 B纪念券(以下简称B券).每个持有金券的顾客都有一个自己的帐户.金券的数目可以是一个实数.每天随着市场的起伏波动,两种金券都有自己当时的价值,即每一单位金券当天可以兑换的人民币数目.我们记录第 K 天中 A券 和 B券 的价值分别为 AK 和 BK(元/单位金券).为了方便顾客,金券交易所提供了一种非常方便的交易方式:比例交易

bzoj1492[NOI2007]货币兑换Cash cdq分治+斜率优化dp

1492: [NOI2007]货币兑换Cash Time Limit: 5 Sec  Memory Limit: 64 MBSubmit: 5541  Solved: 2228[Submit][Status][Discuss] Description 小Y最近在一家金券交易所工作.该金券交易所只发行交易两种金券:A纪念券(以下简称A券)和 B纪念券(以下 简称B券).每个持有金券的顾客都有一个自己的帐户.金券的数目可以是一个实数.每天随着市场的起伏波动, 两种金券都有自己当时的价值,即每一单位金

bzoj千题计划237:bzoj1492: [NOI2007]货币兑换Cash

http://www.lydsy.com/JudgeOnline/problem.php?id=1492 dp[i] 表示 第i天卖完的最大收益 朴素的dp: 枚举从哪一天买来的在第i天卖掉,或者是不操作 dp[i]=max(dp[i-1],X[j]*A[i]+Y[j]*B[i]) 其中X[j]表示在第j天能买多少A纪念券,Y[j]表示在第j天能买多少B纪念券 可列方程 X[j]*A[j]+Y[j]*B[j]=dp[j] 又因为 X[j]=Rate[j]*Y[j] 所以解出 Y[j]=dp[j]

[BZOJ1492] [NOI2007]货币兑换Cash 斜率优化+cdq/平衡树维护凸包

1492: [NOI2007]货币兑换Cash Time Limit: 5 Sec  Memory Limit: 64 MBSubmit: 5907  Solved: 2377[Submit][Status][Discuss] Description 小Y最近在一家金券交易所工作.该金券交易所只发行交易两种金券:A纪念券(以下简称A券)和 B纪念券(以下 简称B券).每个持有金券的顾客都有一个自己的帐户.金券的数目可以是一个实数.每天随着市场的起伏波动, 两种金券都有自己当时的价值,即每一单位金

【BZOJ-1492】货币兑换Cash DP + 斜率优化 + CDQ分治

1492: [NOI2007]货币兑换Cash Time Limit: 5 Sec  Memory Limit: 64 MBSubmit: 3396  Solved: 1434[Submit][Status][Discuss] Description 小Y最近在一家金券交易所工作.该金券交易所只发行交易两种金券:A纪念券(以下简称A券)和 B纪念券(以下 简称B券).每个持有金券的顾客都有一个自己的帐户.金券的数目可以是一个实数.每天随着市场的起伏波动, 两种金券都有自己当时的价值,即每一单位金

BZOJ 1492: [NOI2007]货币兑换Cash( dp + 平衡树 )

dp(i) = max(dp(i-1), x[j]*a[i]+y[j]*b[i]), 0<j<i. x, y表示某天拥有的最多钱去买金券, 金券a和金券b的数量. 然后就很明显了...平衡树维护上凸壳, 询问时就在凸壳上二分...时间复杂度O(NlogN) ----------------------------------------------------------------------------------------------- #include<cmath> #i