peiwenjun's blog 没有知识的荒原

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\)

可以列出方程:

\[a_j\cdot r_j\cdot x+b_j\cdot x=dp_j\\ \]

解得:

\[x=\frac{dp_j}{a_j\cdot r_j+b_j}\\ \]

\(i\) 天卖出的收益为:

\[a_i\cdot r_j\cdot x+b_i\cdot x=\frac{a_i\cdot r_j+b_i}{a_j\cdot r_j+b_j}\cdot dp_j\\ \]

于是可以写出转移方程:

\[dp_i=\max\left(dp_{i-1},\max_{1\le j\lt i}\frac{a_i\cdot r_j+b_i}{a_j\cdot r_j+b_j}\cdot dp_j\right)\\ \]

时间复杂度 \(\mathcal O(n^2)\) ,期望得分 \(60\)

先从转移式中提出 \(a_i\)

\[\frac{a_i\cdot r_j+b_i}{a_j\cdot r_j+b_j}\cdot dp_j=a_i\cdot\left(\frac{dp_j}{a_j\cdot r_j+b_j}\cdot\frac{b_i}{a_i}+\frac{dp_j\cdot r_j}{a_j\cdot r_j+b_j}\right)\\ \]

接下来有好几条路可走。

李超线段树

每个 \(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;
}

posted on 2022-08-18 20:56  peiwenjun  阅读(7)  评论(0)    收藏  举报

导航