1492: [NOI2007]货币兑换Cash DP+斜率优化+splay维护凸包/CDQ分治

博客讲述了利用动态规划(Cash DP)结合斜率优化来解决货币兑换问题,并介绍了CDQ分治的思想,通过splay树维护凸包来提高效率,最终实现复杂度为O(nlogn)的解决方案。作者还提到此算法的优越性以及自己对CDQ分治技术的初步理解。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

似乎两个月之前我写过一次这题写挂了?
《多年的心头大恨终于解决了系列》
我们可以发现,存在最优解满足每次将全部的钱买了券或每次将全部的券卖成钱,因为有利益我们要尽可能的去获得,有亏损就一点也不要碰。
我们用fi表示到i天时的最大收益,令xi,yi分别表示第i天购买A券和B券的数量。则如果将第i天的收益全部买成券,那么满足

fi=Aixi+Biyi

因为有xiyi=ratei,我们代入上式,可以得到:
yi=fiAiratei+Bi,xi=yiratei=firateiAiratei+Bi

状态转移方程显然:
fi=max{Aixj+Biyj}

变形得:
yj=AiBixj+fiBi

可以发现这是一个直线方程的形式,其中斜率为AiBi,截距为fiBi,我们要使fi最大化,也就是使截距最大化。
我们知道决策点一定在一个上凸包上,而对于一些点的横坐标xi递增的题目,我们可以用单调队列来维护一个凸包,如果每个i对应的斜率也单调,我们可以根据决策单调性来线性维护,如果斜率不单调,我们可以在单调队列上二分。而这道题目点的坐标和斜率都不单调,我们需要动态维护一个凸包,并且支持加点,删点,查询前后点等操作,因此可以用splay来维护。
Time:936 ms Code_Length:2550 B,可以发现是比较优秀的。

然后还有一种比较神奇的方法,叫做cdq分治。
主要思想就是离线处理,化无序为有序
我们可以发现,每个点对应的斜率为AiBi,与其他值无关,也就是说一个点xi引入后,对后面的点产生的影响是确定的。
我们考虑分治。
定义这样一个过程solve(l,r),假设调用后可以求得fi(l<=i<=r)。那么对于区间[l,mid]的询问,可以直接递归调用solve(l,mid)得到,对于区间[mid+1,r]的询问K,会受到[l,mid]以及[mid+1,K]的影响,后半部分可以递归解决。我们考虑前半部分。
我们整体考虑[l,mid][mid+1,r]的影响,第一个区间的每一个点都可能成为第二个区间某一个询问的决策点,那么我们不妨维护第一个区间对应点集的凸包,然后把第二个区间的询问按斜率排序,这样O(n)的时间内就可以完成所有的查询。
具体的说,我们首先对所有询问按照斜率排序,然后在分治的时候,按照位置在mid的左右分类,这样形成了两个按照斜率排好序的区间然后递归。然后合并的时候,就对点集进行归并排序。整个过程的复杂度是O(nlogn)的。
可见这个算法是非常优秀的,更多关于CDQ分治的东西还需要我去探究,这只是做的第一道入门的题目,理解的还不是很深啊。
Time:1204 ms Code_Length:1832 B,代码量减小了不少。其实我觉得还是splay写起来顺手

splay

#include<iostream>
#include<cstdio>
#include<cmath>
#define eps 1e-9
#define inf 1e9
#define N 100005
using namespace std;
int fa[N],tree[N][2];
double f[N],x[N],y[N],a[N],b[N],lk[N],rk[N],rate[N];
int n,m,root;
inline void rotate(int x,int &k)
{
    int y=fa[x],z=fa[y],l=tree[y][1]==x,r=l^1;
    if (y==k) k=x; else tree[z][tree[z][1]==y]=x;
    fa[x]=z; fa[y]=x; fa[tree[x][r]]=y;
    tree[y][l]=tree[x][r]; tree[x][r]=y;
}
inline void splay(int x,int &k)
{
    while (x!=k)
    {
        int y=fa[x],z=fa[y];
        if (y!=k)
        {
            if (tree[y][0]==x^tree[z][0]==y) rotate(x,k); else rotate(y,k);
        }
        rotate(x,k);
    }
}
inline void insert(int &k,int f,int id)
{
    if (!k)
    {
        k=id;
        fa[k]=f;
        splay(k,root);
        return;
    }
    if (x[id]<=x[k]+eps) insert(tree[k][0],k,id);
    else insert(tree[k][1],k,id);
}
inline double getk(int i,int j)
{
    if (fabs(x[i]-x[j])<eps) return -inf;
    else return (y[j]-y[i])/(x[j]-x[i]);
}
inline int prev(int k)
{
    int t=tree[k][0],tmp=t;
    while (t)
    {
        if (getk(t,k)<=lk[t]+eps) tmp=t,t=tree[t][1];
        else t=tree[t][0];
    }
    return tmp;
}
inline int succ(int k)
{
    int t=tree[k][1],tmp=t;
    while (t)
    {
        if (getk(t,k)+eps>=rk[t]) tmp=t,t=tree[t][0];
        else t=tree[t][1];
    }
    return tmp;
}
inline void update(int k)
{
    splay(k,root);
    if (tree[k][0])
    {
        int left=prev(root);
        splay(left,tree[k][0]); tree[left][1]=0;
        lk[k]=rk[left]=getk(left,k);
    }
    else lk[k]=inf;
    if (tree[k][1])
    {
        int right=succ(root);
        splay(right,tree[k][1]); tree[right][0]=0;
        rk[k]=lk[right]=getk(right,k);
    }
    else rk[k]=-inf;
    if (lk[k]<=rk[k]+eps)
    {
        root=tree[k][0]; tree[root][1]=tree[k][1]; 
        fa[tree[k][1]]=root; fa[root]=0;
        rk[root]=lk[tree[k][1]]=getk(root,tree[k][1]);
    }
}
inline int find(int k,double slop)
{
    if (!k) return 0;
    if (lk[k]+eps>=slop&&slop+eps>=rk[k]) return k;
    else if (slop+eps>lk[k]) return find(tree[k][0],slop);
    else return find(tree[k][1],slop);
}
int main()
{
    scanf("%d%lf",&n,&f[0]);
    for (int i=1;i<=n;i++) scanf("%lf%lf%lf",&a[i],&b[i],&rate[i]);
    for (int i=1;i<=n;i++)
    {
        int j=find(root,-a[i]/b[i]);
        f[i]=max(f[i-1],x[j]*a[i]+y[j]*b[i]);
        y[i]=f[i]/(a[i]*rate[i]+b[i]);
        x[i]=y[i]*rate[i];
        insert(root,0,i);
        update(i);
    }
    printf("%.3lf\n",f[n]);
    return 0;
}

cdq

#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#define inf 1e9
#define eps 1e-9
#define N 100005
using namespace std;
struct query {int id; double a,b,k,rate;} q[N],nq[N];
struct point {double x,y;} p[N],np[N];
double f[N];
int stack[N];
int n;
inline bool operator<(query a,query b)
{
    return a.k<b.k;
}
inline bool operator<(point a,point b)
{
    return fabs(a.x-b.x)<=eps?a.y<b.y+eps:a.x<b.x+eps;
}
inline double slop(int i,int j)
{
    if (!i) return -inf;
    if (!j) return inf;
    if (fabs(p[i].x-p[j].x)<=eps) return -inf;
    return (p[i].y-p[j].y)/(p[i].x-p[j].x);
}
void solve(int l,int r)
{
    if (l==r)
    {
        f[l]=max(f[l],f[l-1]);
        p[l].y=f[l]/(q[l].a*q[l].rate+q[l].b);
        p[l].x=p[l].y*q[l].rate;
        return;
    }
    int mid=l+r>>1,p1=l,p2=mid+1;
    for (int i=l;i<=r;i++)
        if (q[i].id<=mid) nq[p1++]=q[i];
        else nq[p2++]=q[i];
    for (int i=l;i<=r;i++) q[i]=nq[i];
    solve(l,mid);
    int top=0;
    for (int i=l;i<=mid;i++)
    {
        while (top>=2&&slop(i,stack[top])+eps>slop(stack[top],stack[top-1])) top--;
        stack[++top]=i;
    }
    int j=1;
    for (int i=r;i>=mid+1;i--)
    {
        while(j<top&&q[i].k<slop(stack[j],stack[j+1])+eps) j++;
        f[q[i].id]=max(f[q[i].id],q[i].a*p[stack[j]].x+q[i].b*p[stack[j]].y);
    }
    solve(mid+1,r);
    p1=l; p2=mid+1;
    for (int i=l;i<=r;i++)
        if ((p[p1]<p[p2]||p2>r)&&p1<=mid) np[i]=p[p1++]; else np[i]=p[p2++];
    for (int i=l;i<=r;i++) p[i]=np[i];
}
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].rate);
        q[i].k=-q[i].a/q[i].b;
        q[i].id=i;
    }
    sort(q+1,q+n+1);
    solve(1,n);
    printf("%.3lf\n",f[n]);
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值