P4027 [NOI2007]货币兑换 题解
Post time: 2020-12-02 21:08:23
题意简述:传送门
一共 \(n\) 天,每天可以卖出或者买入两种股票 \(A\) 和 \(B\)。这两种股票在第 \(i\) 天的价值为 \(A_i\) 和 \(B_i\)。
每天可以花所有的现金买入股票,这些股票中 \(A\) 股与 \(B\) 股的数量比为 \(Rate_i\)。每天也可以把所有的股票以当天的价值卖出,获得现金。已知接下来 \(n\) 天的 \(A_i,B_i,Rate_i\),求出 \(n\) 天后能够获得的最大价值。
请注意本题冗余描述过多,其实只有数据范围下面那句话是有用的,做题的时候一定要注意提取关键信息!
本人的解法:cdq 分治维护单调栈斜率优化 dp
考虑到每一次买花掉全部的钱,每一次卖全部卖掉,我们不妨设 \(f_i\) 表示第 \(i\) 天能够拥有的最大钱数,把买股票算在之后每一天当中,即:
设 \(x_k\) 表示第 \(k\) 天买的 \(A\) 股数量,\(y_k\) 表示第 \(k\) 天买的 \(B\) 股数量,\(a_k,b_k,r_k\) 分别表示第 \(k\) 天的 \(A\) 股价值、\(B\) 股价值和 \(AB\) 两股数量比,可得:
解得
接下来我们考虑第 \(i\) 天,卖股票能够得到的最大价值,即我们从 \(1\) 到 \(i-1\) 枚举一个 \(j\),那么第 \(i\) 天就能卖掉第 \(j\) 天买的 \(A\) 股(数量 \(x_j\)),\(B\) 股(数量 \(y_j\))。所以我们可以得到这样一个方程:
暴力的时间复杂度是 \(O(n^2)\),期望得分 \(60\)。
接下来,由于出现了 \(i,j\) 相乘项,考虑斜率优化。首先把它化成斜截式方程,即:
它的意义是在平面内有很多点 \((x_j,y_j)\),对每个 \(i\) 找到一条斜率为 \(-\frac{a_i}{b_i}\) 的直线穿过其中某个点能够得到的最大截距。
做法很明显,维护一个上凸壳,对每个 \(k_i\) 找最优决策点即可。然而——这道题的 \(x_i\) 和 \(k_i\) 无序,所以一无法使用单调队列(需要 \(x_i\) 和 \(k_i\) 均有序),二无法使用单调栈内二分(\(k_i\) 需有序)。这时候我们需要使用 cdq 分治来维护。
具体维护方法如下(把每一天需要的所有状态放进一个 struct
数组中):
- 在进入
cdq(l,r)
之前,先把整个数组按照 \(k\) 排序。 - 当 \(l=r\) 时,更新第 \(l\) 个状态的 \(x_l,y_l\)。
- 把
[l,r]
中天数 \(\leq mid\) 的放在左半边,剩下的放在右半边。 - 递归处理出
[l,mid]
天的 \(f_i,x_i,y_i\)。 - 用单调栈统计
[l,mid]
天对[mid+1,r]
天的贡献。 - 递归处理
[mid+1,r]
天的 \(f_i,x_i,y_i\)。 - 将整个区间
[l,r]
按照 \(x\) 递增排序。
我们可以通过这些步骤,观察出 cdq 分治维护的优秀性质:在第 4 步,能用单调栈线性统计的原因是,对于 [l,mid]
,因为我们在第 7 步把 [l,mid]
按照 \(x\) 单增排序了,同时,在第 0 步对整个区间按照 \(k\) 单增排序了,第一步的操作会使 \(k\) 在 [mid+1,r]
上单增。所以可以使用单调栈统计。
其实根本原因还是在于 cdq 分治的优越性:只需要考虑 [l,mid]
对 [mid+1,r]
的贡献,所以可以分别排序,把原本无法维护的东西花一个 \(\log\) 的代价变成可以维护。
此题总的时间复杂度 \(O(n \log n)\)。
最后放上 AC 代码:
点击查看代码
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
using namespace std;
const int N=1e5+13;
const double eps=1e-9;
const double INF=1e9;
struct Node{double a,b,r,k,x,y;int id;}Q[N],Tmp[N];
struct Queue{//本人比较习惯手写队列(这题好像是个栈,不过不重要)
int q[N],head,tail;
inline void clear(){q[head=tail=1]=0;}
Queue(){clear();}
inline void push(int x){q[++tail]=x;}
inline void pop(){--tail;}
inline int top(){return q[tail];}
inline int ttop(){return q[tail-1];}
inline bool empty(){return head>=tail;}
}s;
double f[N];int n;
inline bool cmp0(const Node &a,const Node &b){return a.k<b.k;}
inline bool cmp1(const Node &a,const Node &b){return a.x<b.x;}
inline double slope(int i,int j){
if(fabs(Q[i].x-Q[j].x)<eps) return INF;//这里没判会WA on #6,#7!!
return (Q[i].y-Q[j].y)/(Q[i].x-Q[j].x);
}
void cdq(int l,int r){
if(l==r){//第1步
f[l]=max(f[l],f[l-1]);//别忘了还可以在这一天当中什么都不干
Q[l].y=f[l]/(Q[l].a*Q[l].r+Q[l].b),Q[l].x=Q[l].r*Q[l].y;//更新x,y的值,接下来可以用它们去更新其他天的f值。
return;
}
int mid=(l+r)>>1;
int l1=l-1,l2=mid;
for(int i=l;i<=r;++i){//第2步,将[l,r]化为左右两个k单调的区间,将询问放在相应的区间内处理
if(Q[i].id<=mid) Tmp[++l1]=Q[i];
else Tmp[++l2]=Q[i];
}
for(int i=l;i<=r;++i) Q[i]=Tmp[i];
cdq(l,mid);//第3步
s.clear();
for(int i=l;i<=mid;++i){//预处理出上凸包
while(!s.empty()&&slope(s.top(),i+eps)>slope(s.ttop(),s.top())) s.pop();
s.push(i);
}
for(int i=mid+1,j;i<=r;++i){//第4步
while(!s.empty()&&Q[i].k+eps>slope(s.ttop(),s.top())) s.pop();//因为斜率递增所以可以直接pop
f[Q[i].id]=max(f[Q[i].id],Q[j=s.top()].x*Q[i].a+Q[j].y*Q[i].b);
}
cdq(mid+1,r);//第5步
sort(Q+l,Q+r+1,cmp1);//第6步,偷懒了没写归并,这样应该也没问题
}
int main(){
scanf("%d%lf",&n,&f[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;//预处理k和id
}
sort(Q+1,Q+n+1,cmp0);//第0步
cdq(1,n);
printf("%.3lf\n",f[n]);
return 0;
}