BZOJ 1492 货币兑换Cash(CDQ分治+斜率优化dp)

版权声明:本文为博主原创文章,转载请著名出处 http://blog.csdn.net/u013534123 https://blog.csdn.net/u013534123/article/details/78691325

        之前已经用斜率优化dp+平衡树维护凸点把这题给解决了,但是呢,这题的故事并没有结束。

        首先膜拜CDQ陈丹琦大神Orz……昨天突然得知,其父亲是NUDT数学系的教授……与大神之间的距离居然如此之近。还是上论文:从《Cash》谈一类分治算法的应用

        显然,用平衡树维护凸点的做法是可以的,但是在比赛中splay却是非常的不好写,编程复杂度极其的高。所以在这种情况下,CDQ就想出了这样一种分治法。首先,我们先回顾一下这道题目。根据分析可以有状态转移方程:dp[i]=max(dp[i-1],x[j]*A[i]+y[j]*B[i]),其中dp表示到第i天为止的最大利润,x、y为对应天的在最优情况下取A券的数量和B券的数量。然后可以用斜率优化,写出如下斜率表达式:(y[j]-y[k])/(x[j]-x[k])<-A[i]/B[i]。矛盾点在于,x[]和-A[i]/B[i]没有任何单调性,无法用单调队列进行斜率优化。

        我们再解释一下这个斜率表达式,对于一个状态i,我们相当于找一个满足斜率不等式的最大的一个决策j,而这个决策显然满足0<j<i。有了这个性质,我们就可以考虑进行分治。因为对于每一个状态i,她都只依赖于1~i-1的决策,所以对于一个需要解决的区间[l,r],我们把它分为[l,mid]和[mid+1,r]两个部分。首先解决前面的部分,然后根据前一半的结果去更新后一半的答案,这就是CDQ分治的精髓。具体到本题,我们首先要对所有状态按照-A[i]/B[i]排序,然后再进行分治,这个目的在于使得最后选取最优决策的时候可以利用这个单调性。对于前半部分,我们先分治它,然后对于后半部分就以前半部分的结果为决策,按照普通斜率优化那样用单调队列优化更新结果。可能你会说,我们还没有解决x[]没有单调性的问题,如果这个不解决的话点加入的顺序就会混乱导致出错。但是不要着急,我们再更新完后半部分的结果之后,再分治后半部分。最后,最关键的一步,我们已经处理完[l,r]区间之后,我们在对每一个已经解决的决策以x为关键字进行归并排序。如此一来,我们每次分治处理完左半区间后,左半区间就已经是按照x的顺序重新排序了的,这样就可以安全的使用单调队列进行优化。这一步不可谓不巧妙,先分后治,分到单位决策直接解决,然后前面更新后面,处理完全部后在决策重排,供后面的状态决策。

        对于本题,更形象的说明这个过程。对于一个状态i,我们要找一个满足条件的最大的决策j,而这个决策在1~i-1之间。而CDQ分治的过程,相当于把1~i-1这个区间分成了几个小区间,然后用每个小区间的最有决策来更新最后结果,如此一来可以保证答案的正确性。至于复杂度的话,可以证明是O(NlogN)。具体来说,这个分治法充分利用了这种先分后治的思想,在很多地方都可以应用,还需要我不断探索。具体见代码:

#include<bits/stdc++.h>
#define INF 1e18
#define N 100010
#define eps 1e-10
using namespace std;

struct node
{
    double k,rate,x,y,a,b;
    int id;
} p[N],tmp[N];

bool cmp1(node a,node b)
{
    return a.k>b.k;
}

bool cmp2(node a,node b)
{
    return a.x<b.x;
}

double dp[N];
int n,m,q[N];

double slope(int a,int b)
{
    if (!b) return -INF;
    if (fabs(p[a].x-p[b].x)<eps) return INF;
    return (p[a].y-p[b].y)/(p[a].x-p[b].x);
}

void cdq(int l,int r)
{
    if (l==r)			//单位区间,直接计算结果,并记录(x,y)坐标
    {
        dp[l]=max(dp[l],dp[l-1]);
        p[l].y=dp[l]/(p[l].a*p[l].rate+p[l].b);
        p[l].x=p[l].y*p[l].rate; return;
    }
    int mid=(l+r)>>1,t1=l-1,t2=mid;
    for(int i=l;i<=r;i++)//按照id分为左右两个部分,而且各个部分内按照-A[i]/B[i]的顺序排好了序
        if (p[i].id>mid) tmp[++t2]=p[i];
                    else tmp[++t1]=p[i];
    for(int i=l;i<=r;i++) p[i]=tmp[i];
    cdq(l,mid);										//分治左半区间
    int h=1,t=0;					//单调队列指针
    for(int i=l;i<=mid;i++)		//利用左半区间的结果作为决策,维护斜率单调下降区间
    {
        while(t>1&&slope(q[t-1],q[t])<slope(q[t],i)+eps) t--;
        q[++t]=i;
    }
    q[++t]=0;
    for(int i=mid+1;i<=r;i++)	//由于已经按照降序排好序,所以直接单调下去更新每一个状态的结果
    {
        while(h<t&&slope(q[h],q[h+1])+eps>p[i].k) h++;
        dp[p[i].id]=max(dp[p[i].id],p[q[h]].x*p[i].a+p[q[h]].y*p[i].b);
    }
    cdq(mid+1,r);			//分治右边
    merge(p+l,p+mid+1,p+mid+1,p+r+1,tmp+l,cmp2); //关键一步,整个区间计算完毕后要按照x
    for(int i=l;i<=r;i++) p[i]=tmp[i];            //来升序排序
}

int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
    {
        double a,b,rate;
        scanf("%lf%lf%lf",&a,&b,&rate);
        p[i]=node{-a/b,rate,0,0,a,b,i};
    }
    sort(p+1,p+1+n,cmp1);
    dp[0]=m; cdq(1,n);
    printf("%.3f",dp[n]);
}

      这个方法真的是太厉害了,对于CDQ本人不得不服○| ̄|_   ○| ̄|_   ○| ̄|_

猜你喜欢

转载自blog.csdn.net/u013534123/article/details/78691325