您的位置:首页 > 其它

BZOJ 1492 货币兑换Cash(CDQ分治+斜率优化dp)

2017-12-01 21:40 302 查看

1492: [NOI2007]货币兑换Cash

Time Limit: 5 Sec  Memory Limit:
64 MB
Submit: 5418  Solved: 2189

[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

        之前已经用斜率优化dp+平衡树维护凸点把这题给解决了,但是呢,这题的故事并没有结束。

        首先膜拜CDQ陈丹琦大神Orz……昨天突然得知,其父亲是NUDT数学系的教授……与大神之间的距离居然如此之近。还是上论文:从《Cash》谈一类分治算法的应用

        显然,用平衡树维护凸点的做法是可以的,但是在比赛中splay却是非常的不好写,编程复杂度极其的高。所以在这种情况下,CDQ就想出了这样一种分治法。首先,我们先回顾一下这道题目。根据分析可以有状态转移方程:dp[i]=max(dp[i-1],x[j]*A[i]+y[j]*B[i]),其中dp表示到第i天为止的最大利润,x、y为对应天的在最优情况下取A券的数量和B券的数量。然后可以用斜率优化,写出如下斜率表达式:(y[j]-y[k])/(x[j]-x[k])<-A[i]/B[i]。矛盾点在于,x[]和-A[i]/B[i]没有任何单调性,无法用单调队列进行斜率优化。

        我们再解释一下这个斜率表达式,对于一个状态i,我们相当于找一个满足斜率不等式的最大的一个决策j,而这个决策显然满足0<j<i。有了这个性质,我们就可以考虑进行分治。因为对于每一个状态i,她都只依赖于1~i-1的决策,所以对于一个需要解决的区间[l,r],我们把它分为[l,mid]和[mid+1,r]两个部分。首先解决前面的部分,然后根据前一半的结果去更新后一半的答案,这就是CDQ分治的精髓。具体到本题,我们首先要对所有状态按照-A[i]/B[i]排序,然后再进行分治,这个目的在于使得最后选取最优决策的时候可以利用这个单调性。对于前半部分,我们先分治它,然后对于后半部分就以前半部分的结果为决策,按照普通斜率优化那样用单调队列优化更新结果。可能你会说,我们还没有解决x[]没有单调性的问题,如果这个不解决的话点加入的顺序就会混乱导致出错。但是不要着急,我们再更新完后半部分的结果之后,再分治后半部分。最后,最关键的一步,我们已经处理完[l,r]区间之后,我们在对每一个已经解决的决策以x为关键字进行归并排序。如此一来,我们每次分治处理完左半区间后,左半区间就已经是按照x的顺序重新排序了的,这样就可以安全的使用单调队列进行优化。这一步不可谓不巧妙,先分后治,分到单位决策直接解决,然后前面更新后面,处理完全部后在决策重排,供后面的状态决策。

        对于本题,更形象的说明这个过程。对于一个状态i,我们要找一个满足条件的最大的决策j,而这个决策在1~i-1之间。而CDQ分治的过程,相当于把1~i-1这个区间分成了几个小区间,然后用每个小区间的最有决策来更新最后结果,如此一来可以保证答案的正确性。至于复杂度的话,可以证明是O(NlogN)。具体来说,这个分治法充分利用了这种先分后治的思想,在很多地方都可以应用,还需要我不断探索。具体见代码:

#include<bits/stdc++.h>
#define INF 1e18
#define N 100010
#define eps 1e-10
using namespace std;

struct node
{
double k,rate,x,y,a,b;
int id;
} p
,tmp
;

bool cmp1(node a,node b)
{
return a.k>b.k;
}

bool cmp2(node a,node b)
{
return a.x<b.x;
}

double dp
;
int n,m,q
;

double slope(int a,int b)
{
if (!b) return -INF;
if (fabs(p[a].x-p[b].x)<eps) return INF;
return (p[a].y-p[b].y)/(p[a].x-p[b].x);
}

void cdq(int l,int r)
{
if (l==r)						//单位区间,直接计算结果,并记录(x,y)坐标
{
dp[l]=max(dp[l],dp[l-1]);
p[l].y=dp[l]/(p[l].a*p[l].rate+p[l].b);
p[l].x=p[l].y*p[l].rate; return;
}
int mid=(l+r)>>1,t1=l-1,t2=mid;
for(int i=l;i<=r;i++)					//按照id分为左右两个部分,而且各个部分内按照-A[i]/B[i]的顺序排好了序
if (p[i].id>mid) tmp[++t2]=p[i];
else tmp[++t1]=p[i];
for(int i=l;i<=r;i++) p[i]=tmp[i];
cdq(l,mid);										//分治左半区间
int h=1,t=0;						//单调队列指针
for(int i=l;i<=mid;i++)					//利用左半区间的结果作为决策,维护斜率单调下降区间
{
while(t>1&&slope(q[t-1],q[t])<slope(q[t],i)+eps) t--;
q[++t]=i;
}
q[++t]=0;
for(int i=mid+1;i<=r;i++)					//由于已经按照降序排好序,所以直接单调下去更新每一个状态的结果
{
while(h<t&&slope(q[h],q[h+1])+eps>p[i].k) h++;
dp[p[i].id]=max(dp[p[i].id],p[q[h]].x*p[i].a+p[q[h]].y*p[i].b);
}
cdq(mid+1,r);									//分治右边
merge(p+l,p+mid+1,p+mid+1,p+r+1,tmp+l,cmp2);		//关键一步,整个区间计算完毕后要按照x来升序排序
for(int i=l;i<=r;i++) p[i]=tmp[i];
}

int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
double a,b,rate;
scanf("%lf%lf%lf",&a,&b,&rate);
p[i]=node{-a/b,rate,0,0,a,b,i};
}
sort(p+1,p+1+n,cmp1);
dp[0]=m; cdq(1,n);
printf("%.3f",dp
);
}


      这个方法真的是太厉害了,对于CDQ本人不得不服○| ̄|_   ○| ̄|_   ○| ̄|_
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: