cunzai_zsy0531

关注我

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\) 两股数量比,可得:

\[\left\{ \begin{aligned} & x_k\cdot a_k+y_k\cdot b_k=f_k \\ & \frac{x_k}{y_k}=r_k \\ \end{aligned} \right. \]

解得

\[\left\{ \begin{aligned} & x_k=\frac{f_k r_k}{a_k r_k+b_k} \\ & y_k=\frac{f_k}{a_k r_k+b_k} \\ \end{aligned} \right. \]

接下来我们考虑第 \(i\) 天,卖股票能够得到的最大价值,即我们从 \(1\)\(i-1\) 枚举一个 \(j\),那么第 \(i\) 天就能卖掉第 \(j\) 天买的 \(A\) 股(数量 \(x_j\)),\(B\) 股(数量 \(y_j\))。所以我们可以得到这样一个方程:

\[f_i=a_ix_j+b_iy_j \]

暴力的时间复杂度是 \(O(n^2)\),期望得分 \(60\)

接下来,由于出现了 \(i,j\) 相乘项,考虑斜率优化。首先把它化成斜截式方程,即:

\[y_j=-\frac{a_i}{b_i}x_j+\frac{f_i}{b_i} \]

它的意义是在平面内有很多点 \((x_j,y_j)\),对每个 \(i\) 找到一条斜率为 \(-\frac{a_i}{b_i}\) 的直线穿过其中某个点能够得到的最大截距。

做法很明显,维护一个上凸壳,对每个 \(k_i\) 找最优决策点即可。然而——这道题的 \(x_i\)\(k_i\) 无序,所以一无法使用单调队列(需要 \(x_i\)\(k_i\) 均有序),二无法使用单调栈内二分(\(k_i\) 需有序)。这时候我们需要使用 cdq 分治来维护。

具体维护方法如下(把每一天需要的所有状态放进一个 struct 数组中):

  1. 在进入 cdq(l,r) 之前,先把整个数组按照 \(k\) 排序。
  2. \(l=r\) 时,更新第 \(l\) 个状态的 \(x_l,y_l\)
  3. [l,r] 中天数 \(\leq mid\) 的放在左半边,剩下的放在右半边。
  4. 递归处理出 [l,mid] 天的 \(f_i,x_i,y_i\)
  5. 用单调栈统计 [l,mid] 天对 [mid+1,r] 天的贡献。
  6. 递归处理 [mid+1,r] 天的 \(f_i,x_i,y_i\)
  7. 将整个区间 [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;
}
posted @ 2022-04-21 14:51  cunzai_zsy0531  阅读(41)  评论(0编辑  收藏  举报