[NOI2007]货币兑换
好像是二刷了吧,但是肯定不止见过两次了。
壹、题目描述
贰、蒟蒻の思考
不难发现 题目提示也给了 必定存在一种最优方案满足买入时用完所有钱,卖出时卖完所有股票。
先分析清楚题,假如说在第 \(i\) 天用 \(S\) 元买股票,那么最后
那么我们定义 \(f_i\) 表示第 \(i\) 天能够得到的最多的钱,那么就有一个 \(\mathcal O(n^2)\) 的转移:
最后还要 \(f_i=\max\{f_i,f_{i-1}\}\) 表示今天什么都没干,初始状态 \(f_0=s\).
十分容易地拿下 \(60pts\),考虑优化这个 \(\mathcal O(n^2)\) 的 \(\tt DP\).
定义 \(t_i=A_i\text{Rate}_i+B_i\),那么转移写作
发现这两个分数都只和 \(j\) 有关,那么这个式子就是可以斜率优化的。
记 \(x_j={f_j\text{Rate}_j\over t_j},y_j={f_j\over t_j}\),我们将这个方程改写一下:
从含义上讲,即对于之前的所有决策点,在平面上记其为 \(\lang x_j,y_j\rang\),然后用 \(-{A_i\over B_i}\) 的直线去任意切一个,得到的最大截距就是 \(f_i\over B_i\),然后就可以算出最大 \(f_i\) 了。
但是有一个问题,那就是这个 \(x_j\) 并不是单调的,所以我们还不能使用单调队列对其进行维护,但是我们可以使用 \(\tt CDQ\) 分治,每次把左边的 \(x_i\) 进行排序,考虑左边对右边的贡献,维护一个不动凸包,然后在凸包上面二分即可,这样做复杂度 \(\mathcal O(n\log^2 n)\). 其实也不需要再凸包上进行二分,只需要将右边按照斜率 \(k=-{A_i\over B_i}\) 排序,每次在凸包前面查看是否有更优解,有就将队头弹出,直到找到最优解更新答案即可。
叁、参考代码
#include<cstdio>
#include<algorithm>
using namespace std;
typedef long long ll;
template<class T>inline T fab(const T x){return x<0? -x: x;}
template<class T>inline T readin(T x){
x=0; int f=0; char c;
while((c=getchar())<'0' || '9'<c) if(c=='-') f=1;
for(x=(c^48); '0'<=(c=getchar()) && c<='9'; x=(x<<1)+(x<<3)+(c^48));
return f? -x: x;
}
const int maxn=1e5;
const double eps=1e-8;
typedef struct Double{
double x;
Double(){}
Double(const double X): x(X){}
inline int operator <(const Double rhs) const{
return x<rhs.x;
}
inline int operator >(const Double rhs) const{
return rhs<(*this);
}
inline int operator ==(const Double rhs) const{
return fab(x-rhs.x)<eps;
}
inline int operator <=(const Double rhs) const{
return (*this)<rhs || (*this)==rhs;
}
inline int operator >=(const Double rhs) const{
return (*this)>rhs || (*this)==rhs;
}
inline Double operator -(const Double rhs) const{
return x-rhs.x;
}
inline Double operator +(const Double rhs) const{
return x+rhs.x;
}
inline Double operator *(const Double rhs) const{
return x*rhs.x;
}
inline Double operator /(const Double rhs) const{
return x/rhs.x;
}
inline Double operator -() const{
return -x;
}
} doub;
int n;
doub a[maxn+5], b[maxn+5], rate[maxn+5];
doub x[maxn+5], y[maxn+5], f[maxn+5];
inline void input(){
n=readin(1); scanf("%lf", &f[1].x);
for(int i=1; i<=n; ++i){
scanf("%lf %lf %lf", &a[i].x, &b[i].x, &rate[i].x);
}
}
inline int comp(const doub a,const doub b){
return a.x>b.x;
}
inline int cmpx(const int i, const int j){
return x[i]<x[j];
}
inline int cmpslope(const int i, const int j){
return -a[i]/b[i]>-a[j]/b[j];
}
inline doub slope(const int i, const int j){
if(x[i]==x[j]) return (y[i]-y[j])*1e8;
return (y[i]-y[j])/(x[i]-x[j]);
}
inline doub slope(const int i){
return -a[i]/b[i];
}
int q[maxn+5], op, ed;
int id[maxn+5];
void solve(const int l, const int r){
if(l==r){
y[l]=x[l]/rate[l];
return;
}
int mid=(l+r)>>1;
solve(l, mid);
for(int i=l; i<=r; ++i) id[i]=i;
sort(id+l, id+mid+1, cmpx);
sort(id+mid+1, id+r+1, cmpslope);
op=1, ed=0;
for(int i=l; i<=mid; ++i){
while(op<ed && slope(q[ed-1], q[ed])<slope(q[ed], id[i])) --ed;
q[++ed]=id[i];
}
for(int i=mid+1; i<=r; ++i){
while(op<ed && slope(q[op], q[op+1])>=slope(id[i])) ++op;
f[id[i]]=max(f[id[i]], a[id[i]]*x[q[op]]+b[id[i]]*y[q[op]]);
}
for(int i=mid+1; i<=r; ++i){
f[i]=max(f[i], f[i-1]);
x[i]=rate[i]*f[i]/(a[i]*rate[i]+b[i]);
}
solve(mid+1, r);
}
signed main(){
input();
x[1]=f[1]*rate[1]/(a[1]*rate[1]+b[1]);
solve(1, n);
printf("%.4f\n", f[n].x);
return 0;
}
有些地方精度太高了,调了好久......卡进度真的没意思。
肆、用到の小 \(\tt trick\)
有时候斜率并不能保证一些东西是单调的,这个时候你就需要使用动态凸包或者用 \(\tt CDQ\) 处理这种坐标不保证单调的情况。
另外,对于 \(\tt DP\) 方程的变形,要将和 \(i\) 有关的,和 \(j\) 有关的变量进行剥离,这样才能更好地处理。