bzoj1492 [NOI2007]货币兑换Cash (斜率DP+cdq分治)

来源:互联网 发布:诺基亚n8软件安装 编辑:程序博客网 时间:2024/05/17 02:52

题意:到处都找得到。

我没看错的话当年考试的时候的题面里头,是提示了买卖一定是全部买入和卖出的。这样一来就好办了。cdq的论文里面那个F并不是她所说的那样,而是就是那个最优值。方程转移的时候实际上是枚举j,将第j天的东西全部卖掉,然后在当前的i这一天全部买入。这个方程关系比较复杂,并且网上很多题解都说得含糊不清,所以我昨晚推了一个小时左右才真正搞懂那个方程式以及斜率。

方程很巧,x,y是那一天可以保有的a,b股票的数量,并且刚好是斜率优化当中一个点的横纵坐标。

如果重点不是想学斜率优化而是想学cdq分治的,最好不要再思考这个dp方程了,直接看cdq怎么完成的。


根据斜率优化的思想,将每个决策点抽象成一个点(xi,yi),其中(xi,yi)都是只和i,f[i]相关的量。维护一个斜率递减的曲线,每一次切线上的点就是最优决策点。但是不同于常规的斜率优化,靠一个单调队列是没法做的,因为它的x坐标并不是随着求f[1...n]的过程中递增的。

比较直观的思路就是强力维护这个曲线。网上基本都是用splay写的,代码量巨大,我简直不敢尝试。。(不过它既然要求相邻两个点之间满足的一个关系,分块来保存这个曲线岂不是更简单?)

最简洁的做法是cdq分治,但是对于入门者来说网上的讲解实在过于简单/含糊不清,就我的理解,它的优化机理有点类似线段树或者倍增算法的感觉。每个f[i]的决策区间一定是[1,i-1]。对于每一个决策区间[1,x],对应到线段树上最多只有logn段。每一个f[i]实际要被更新大约logn次,而这些更新足以覆盖所有它可能的决策点。

毕竟这是我第一次做cdq分治,对它的理解可能有些片面。网上这道题的题解说得都很模糊。我看了好久,勉强理解了它的意思。它内部其实就是一个快速排序+一个归并排序。

注意,每一次递归返回之后保证这个区间内f值已经确定且决策点按照字典序排好了。

      1.首先获得N个询问(即f[1..N]),然后按照这N个询问所代表的线的斜率排序(方便转移时候的单调性)。

      2.然后进行分治。分治的时候,最开始拿到一个区间[l,r],其实这个区间是按斜率排序的,而原来的位置(记为id)实际上是被破坏了。我们就要对这段区间进行划分。令mid=(l+r)/2,则将id值<=mid的划分到左边,其余的划分到右边,但是划分到同一边的要保证原来的相对位置不变(可以用STL里的stable_partition)。由于所有id值实际上是1到N的一个排列,所以保证在每一层可以均匀分的,并且分好的两边各自仍然保持斜率的单调

      3.分好之后,递归左边[l,mid]。然后将左边的决策点利用单调队列建成一个斜率递减的曲线,然后用这个曲线来更新[mid+1,r]。由于[mid+1,r]这个区间斜率仍然是单调的,所以可以依次扫描那条曲线,并且可以毫不担心地像普通斜率优化那样弹出队首元素(一系列斜率单调的直线去切一条上凸曲线,切点一定是单调向右走的),但是不同在于,并不将[mid+1,r]的值入队,因为它们之间id值大小不定。具体实现的时候斜率单增单减其实都可以,看你往哪边弹。这样一来,f[mid+1...r]的值都得到了更新,但并没有被确定

      4.上一个步骤以及之前的分治节点已经保证f[mid+1...r]这段区间的值不会再被[1,mid]内的值更新了。所以递归处理右区间[mid+1,r],递归完之后,就可以保证f[mid+1...r]的值是最优了,且[mid+1,r]这段区间的所有决策点也已经按照字典序排好了。

      5.实际上到现在[l,r]这个区间内所有f值已经确定了。但是考虑到这个区间还有父亲节点,为了保证他的父亲节点能够顺利完成,需要将[l,r]内的坐标排序,由于它自己的两个儿子节点是排好了序的,可以用归并排序的方法在线性时间内合并(这里用STL的inplace_merge会比较简洁)。


唔。。大致就是这样,这么几点内容我差不多领悟了一上午,真是太辣鸡啦!不过在严重缺乏详细资料的情况下能领会到这里已经很不错了,感觉学竞赛的确很锻炼自学能力。


#include<iostream>#include<algorithm>#include<cstdio>#include<cstring>#include<cmath>using namespace std;#define DB double#define rep(i,a,b) for(int i=a;i<=b;++i)const int MAXN = 100005;const DB eps = 1e-9;const DB inf = 1e9;int N;struct qua{DB a, b, rate, k;int id;void get(int i) {scanf("%lf%lf%lf", &a, &b, &rate);k = -a/b; id = i;}bool operator < (const qua&b) const {return k < b.k;}} q[MAXN], nq[MAXN];inline bool equal(const DB&a, const DB&b) {return fabs(a - b) <= eps;}struct dot{DB x, y;bool operator < (const dot&b) const{if (equal(x, b.x)) return y < b.y;return x < b.x;}} po[MAXN];DB slo(int i, int j){if (!i) return -inf;if (!j) return inf;if (fabs(po[i].x-po[j].x)<=eps) return -inf;    return (po[i].y-po[j].y) / (po[i].x-po[j].x);}DB f[MAXN];int myq[MAXN];void cdq(int L, int R){if (L == R){f[L] = max(f[L], f[L-1]);po[L].y = f[L] / (q[L].a*q[L].rate + q[L].b);po[L].x = po[L].y * q[L].rate;return;}int mid = (L+R)>>1;int l1 = L, l2 = mid+1;///// 下面这一坨实际上是一个类似快排的划分。///// 可以用系统自带partion函数实现,但是需要外部定义比较器,反而不如手写rep(i, L, R){if (q[i].id <= mid) nq[l1++] = q[i];else nq[l2++] = q[i];}rep(i, L, R) q[i] = nq[i];cdq(L, mid);int l = 1, r = 0;rep(i, L, mid){while (r>=2 && slo(i, myq[r])>slo(myq[r], myq[r-1])) --r;myq[++r] = i;}for (int i = R, t; i>mid; --i){while (l<r && q[i].k<slo(myq[l], myq[l+1])) ++l;t = q[i].id;f[t] = max(f[t], po[myq[l]].x * q[i].a + po[myq[l]].y * q[i].b);}cdq(mid+1, R);//对点排序,实际是归并排序。可以用系统自带的mergeinplace_merge(po+L, po+mid+1, po+R+1);}int main(){scanf("%d%lf", &N, &f[0]);rep(i, 1, N) q[i].get(i);sort(q+1, q+N+1);cdq(1, N);printf("%.3lf\n", f[N]);return 0;}


1 0
原创粉丝点击