P4027 [NOI2007] 货币兑换 题解
题目描述
总共 \(n\) 天,初始你有 \(s\) 元钱。
两种金券,第 \(i\) 天金券的价值分别为 \(a_i,b_i\) ,交易比例 \(r_i\) 。
每天你可以做两件事:
- 卖出金券:指定 \(op\in [0,100]\) ,将 \(op\%\) 的 \(a\) 券和 \(b\) 券按照当天价格兑换成钱。
- 买入金券:花费若干元钱(可以是实数),按照 \(a\) 券和 \(b\) 券的数量比为 \(r_i\) 兑换相应数量的金券。
求 \(n\) 天后你最多有多少钱,保留三位小数。
提示:存在一种最优方案满足,每次买入操作使用所有钱,每次卖出操作使用所有金券。
数据范围
- \(1\le n\le 10^5,0\lt a_i,b_i\le 10,\lt r_i\le 100\) ,保证答案 \(\le 10^9\) 。
时间限制 \(\texttt{1s}\) ,空间限制 \(\texttt{128MB}\) 。
分析
动态规划, \(dp_i\) 表示经过 \(i\) 天最多可以获得多少金券。
如果第 \(i\) 天没有卖出, \(dp_i\gets dp_{i-1}\) 。
否则枚举上一次买入金券在第 \(j\) 天,假设 \(b\) 券的数量为 \(x\) 。
可以列出方程:
解得:
第 \(i\) 天卖出的收益为:
于是可以写出转移方程:
时间复杂度 \(\mathcal O(n^2)\) ,期望得分 \(60\) 。
先从转移式中提出 \(a_i\) :
接下来有好几条路可走。
李超线段树
每个 \(j\) 可以看成贡献了一条斜率为\(\dfrac{dp_j}{a_j\cdot r_j+b_j}\),截距为\(\dfrac{dp_j\cdot r_j}{a_j\cdot r_j+b_j}\)的直线,目标计算 \(\frac{b_i}{a_i}\) 处的最高点,李超线段树维护。
注意需要对\(\frac{b_i}{a_i}\)离散化,时间复杂度 \(\mathcal O(n\log n)\) 。
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+5;
int n;
double a[maxn],b[maxn],c[maxn],r[maxn],dp[maxn];
struct line
{
double k,b;
double calc(int x)
{
return k*c[x]+b;
}
}f[4*maxn];
void insert(int p,int l,int r,line v)
{
int mid=(l+r)/2;
if(v.calc(mid)>f[p].calc(mid)) swap(v,f[p]);
if(v.calc(l)>f[p].calc(l)) insert(2*p,l,mid,v);
if(v.calc(r)>f[p].calc(r)) insert(2*p+1,mid+1,r,v);
}
double query(int p,int l,int r,int pos)
{
double res=f[p].calc(pos);
if(l==r) return res;
int mid=(l+r)/2;
if(pos<=mid) return max(res,query(2*p,l,mid,pos));
else return max(res,query(2*p+1,mid+1,r,pos));
}
int main()
{
scanf("%d%lf",&n,&dp[0]);
for(int i=1;i<=n;i++) scanf("%lf%lf%lf",&a[i],&b[i],&r[i]),c[i]=b[i]/a[i];
sort(c+1,c+n+1);
for(int i=1;i<=n;i++)
{
dp[i]=max(dp[i-1],a[i]*query(1,1,n,lower_bound(c+1,c+n+1,b[i]/a[i])-c));
insert(1,1,n,{dp[i]/(r[i]*a[i]+b[i]),dp[i]*r[i]/(r[i]*a[i]+b[i])});
}
printf("%.3lf",dp[n]);
return 0;
}
CDQ 分治
换一种看待问题的方式。
每个 \(j\) 看成贡献了一个点 \((\dfrac{dp_j}{a_j\cdot r_j+b_j},\dfrac{dp_j\cdot r_j}{a_j\cdot r_j+b_j})\) ,目标计算用斜率为\(-\frac{b_i}{a_i}\)的直线能够得到的最大截距。
CDQ 分治,已知 \([l,mid]\) 的点,回答 \([mid+1,r]\) 的询问。
注意到可以离线,类似斜率优化,建出上凸壳并将询问按照斜率降序排序,扫一遍即可求出所有答案。
注意本题精度卡的比较严,计算斜率时如果两点横坐标相同要特判返回 inf 。
时间复杂度 \(\mathcal O(n\log^2n)\) 。
#include<bits/stdc++.h>
#define fi first
#define se second
#define mp make_pair
#define pii pair<double,int>
#define pdd pair<double,double>
using namespace std;
const int maxn=1e5+5;
const double eps=1e-8,inf=1e8;
int n;
double a[maxn],b[maxn],r[maxn],dp[maxn];
pdd p[maxn],st[maxn];
pii q[maxn];
void chmax(double &x,double y)
{
x=max(x,y);
}
double slope(pdd a,pdd b)
{
if(abs(b.fi-a.fi)<=eps) return inf;
return (b.se-a.se)/(b.fi-a.fi);
}
void cdq(int l,int r)
{
if(l==r) return dp[l]=max(dp[l-1],a[l]*dp[l]),void();
int mid=(l+r)/2;
cdq(l,mid);
int m=0,cnt=0,top=0;
for(int i=l;i<=mid;i++) p[++cnt]={dp[i]/(a[i]*::r[i]+b[i]),dp[i]*::r[i]/(a[i]*::r[i]+b[i])};
sort(p+1,p+cnt+1);
for(int i=1;i<=cnt;i++)
{
while(top>=2&&slope(st[top-1],st[top])<=slope(st[top],p[i])) top--;
st[++top]=p[i];
}
for(int i=mid+1;i<=r;i++) q[++m]=mp(-b[i]/a[i],i);
sort(q+1,q+m+1,greater<pii>());
for(int i=1,j=1;i<=m;i++)
{
while(j!=top&&slope(st[j],st[j+1])>=q[i].fi) j++;
chmax(dp[q[i].se],-st[j].fi*q[i].fi+st[j].se);
}
cdq(mid+1,r);
}
int main()
{
scanf("%d%lf",&n,&dp[0]);
for(int i=1;i<=n;i++) scanf("%lf%lf%lf",&a[i],&b[i],&r[i]);
cdq(1,n);
printf("%.3lf\n",dp[n]);
return 0;
}
平衡树
思路同李超线段树,平衡树动态维护上凸壳。
直线按照斜率排序,额外记录它在凸壳上成为最优直线的左端点 \(p\) 。
插入新直线时,先删掉后面被它完全覆盖的直线;如果它又使前面的直线失效,继续向前维护。查询时按照 \(x=\frac{b_i}{a_i}\) 在 \(p\) 上二分,找到对应区间的最优直线即可。
时间复杂度 \(\mathcal O(n\log n)\) 。
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+5;
const double inf=1e18,eps=1e-12;
int n;
double a[maxn],b[maxn],r[maxn],dp[maxn];
struct line
{
mutable double k,b,p;
bool operator<(const line &o)const
{
return k<o.k;
}
bool operator<(double x)const
{
return p<x;
}
};
struct CHT:multiset<line,less<> >
{
double div(line x,line y)
{
return (y.b-x.b)/(x.k-y.k);
}
bool inter(iterator x,iterator y)
{
if(y==end()) return x->p=inf,0;
if(abs(x->k-y->k)<=eps) x->p=x->b>y->b?inf:-inf;
else x->p=div(*x,*y);
return x->p>=y->p;
}
void insert_line(double k,double b)
{
auto z=insert({k,b,0}),y=z++,x=y;
while(inter(y,z)) z=erase(z);
if(x!=begin()&&inter(--x,y)) inter(x,y=erase(y));
while((y=x)!=begin()&&(--x)->p>=y->p) inter(x,erase(y));
}
double query(double x)
{
line v=*lower_bound(x);
return v.k*x+v.b;
}
}t;
int main()
{
scanf("%d%lf",&n,&dp[0]);
for(int i=1;i<=n;i++) scanf("%lf%lf%lf",&a[i],&b[i],&r[i]);
for(int i=1;i<=n;i++)
{
dp[i]=dp[i-1];
if(!t.empty()) dp[i]=max(dp[i],a[i]*t.query(b[i]/a[i]));
t.insert_line(dp[i]/(a[i]*r[i]+b[i]),dp[i]*r[i]/(a[i]*r[i]+b[i]));
}
printf("%.3lf\n",dp[n]);
return 0;
}
本文来自博客园,作者:peiwenjun,转载请注明原文链接:https://www.cnblogs.com/peiwenjun/p/16600105.html
浙公网安备 33010602011771号