题解 货币兑换
题目大意
有\(n\)天,每天都是同样的两只股票。每天都有三个参数\(A_i,B_i,R_i\),表示当天股票价格,以及买入的\(A,B\)两股数量之比为\(R_i:1\)。
提示:
一定存在最优方案使得每天要么不动,要么全部卖出
思路
这道题拖了好久啊!!!主要是以前对斜率优化不是特别理解(尽管可能以后看来现在的我也不是很理解)(大雾
提示似乎已经写得很清楚了,如果没有这个提示的话估计这个贪心都没办法猜到吧。
我们可以设\(f_i\)为第\(i\)天卖出交易之后收获的最大\(\texttt{money}\),于是我们可以得到\(\texttt{dp}\)转移式:
其中\(x_j,y_j\)表示的是第\(j\)天把所有钱换做股票的\(A,B\)两股的数量。可以得到\(x_j=\dfrac{f_jR_j}{A_jR_j+B_j},y_j=\dfrac{f_j}{A_jR_j+B_j}\)。
于是,我们就可以得到:
如果我们设一条经过原点,斜率为\(-\dfrac{A_i}{B_i}\)的直线为\(Q\),于是,我们要求的就是用\(Q\)去切,使截距最大的\((x_j,y_j)\)。(截距就是当\(x=x_j\)时\(y_j\)与函数值差的绝对值,不理解见下图)
我们可以发现的是,如果当前点与左边点的斜率比\(Q\)的斜率小,那把当前点移到左边点,那么答案肯定更优。类似的,如果当前点与右边点的斜率比\(Q\)的斜率大,那把当前点移到右边点,那么答案肯定更优。大意如下图:
于是,我们就可以得到,我们需要维护的其实是个凸包,最优的答案就在凸包上。(这个自己想一下就可以明白了)这个东西有\(2\)种维护方法,一种是\(\texttt{Splay}\),另外一种是\(\texttt{cdq}\)分治,我采用的前者。
首先很显然的是,\(\texttt{Splay}\)内部以\(x\)坐标为关键字。
很显然,我们查询的答案的话直接按照上面的方法暴力走就好了,因为凸包的斜率单调递减,又因为\(\texttt{Splay}\)的深度是\(\log n\)级别的,所以此操作单次是\(\log n\)的。
我们考虑插入,很显然我们需要找到可以构成凸包的最远左右端点。大意如下图:
updated on 2020-07-15:
其实不是最远左右端点,而是最近的可以构成左右端点的点。
很显然,我们直接把中间的点直接删完即可。
找到最远左右端点直接在\(\texttt{Splay}\)上查找即可,单次时间复杂度为\(\log n\)。
不过需要注意的是,我们还需要把插入点在凸包内的情况排除掉。不过这道题数据很水,即使不判也不会有什么问题。
具体实现的话,直接\(\texttt{Splay}\)里面维护每个凸包上的点在凸包上与左右两端的点的斜率。
这道题也很卡精度,所以尽量不要卡得很严,大于小于的时候多加几个\(\text{eps}\)。
综上,总时间复杂度为\(\Theta(n\log n)\)。
话说斜率优化不就是保证\(x\)坐标单调上升的版本么?(雾
\(\text {Code}\)
#include <bits/stdc++.h>
using namespace std;
#define Int register int
#define MAXN 100005
#define eps 1e-9
#define inf 1e9
template <typename T> inline void read (T &t){t = 0;char c = getchar();int f = 1;while (c < '0' || c > '9'){if (c == '-') f = -f;c = getchar();}while (c >= '0' && c <= '9'){t = (t << 3) + (t << 1) + c - '0';c = getchar();} t *= f;}
template <typename T,typename ... Args> inline void read (T &t,Args&... args){read (t);read (args...);}
template <typename T> inline void write (T x){if (x < 0){x = -x;putchar ('-');}if (x > 9) write (x / 10);putchar (x % 10 + '0');}
int n,rt,fa[MAXN],son[MAXN][2];
double A[MAXN],B[MAXN],R[MAXN],X[MAXN],Y[MAXN],lk[MAXN],rk[MAXN],dp[MAXN];
bool rnk (int x){return son[fa[x]][1] == x;}
void rotate (int x,int &root){
int y = fa[x],z = fa[y],k = rnk (x),w = son[x][!k];
if (y == root) root = x;else son[z][rnk (y)] = x;
son[x][!k] = y,son[y][k] = w,fa[w] = y,fa[x] = z,fa[y] = x;
}
void Splay (int x,int &root){
while (x ^ root){
int y = fa[x];
if (y ^ root) rotate (rnk (x) == rnk (y) ? y : x,root);
rotate (x,root);
}
}
int find (int x,double Slope){
if (!x) return 0;
if (lk[x] + eps >= Slope && rk[x] <= Slope + eps) return x;
else if (lk[x] < Slope + eps) return find (son[x][0],Slope);
else return find (son[x][1],Slope);
}
double GetSlp (int a,int b){
if (X[a] - X[b] > -eps && X[a] - X[b] < eps) return -inf;
return (Y[b] - Y[a]) / (X[b] - X[a]);
}
int pre (int x){
int y = son[x][0],ans = y;
while (y){
if (lk[y] + eps >= GetSlp (y,x)) ans = y,y = son[y][1];
else y = son[y][0];
}
return ans;
}
int suf (int x){
int y = son[x][1],ans = y;
while (y){
if (rk[y] <= GetSlp (x,y) + eps) ans = y,y = son[y][0];
else y = son[y][1];
}
return ans;
}
void ins (int &x,int las,int id){
if (!x) return x = id,fa[x] = las,void ();
if (X[id] <= X[x] + eps) ins (son[x][0],x,id);
else ins (son[x][1],x,id);
}
void Rebuild (int x){
Splay (x,rt);
if (son[x][0]){
int k = pre (x);
Splay (k,son[x][0]),son[k][1] = 0,rk[k] = lk[x] = GetSlp (k,x);
}
else lk[x] = inf;
if (son[x][1]){
int k = suf (x);
Splay (k,son[x][1]),son[k][0] = 0,lk[k] = rk[x] = GetSlp (x,k);
}
else rk[x] = -inf;
if (lk[x] <= rk[x] + eps){
rt = son[x][0],son[rt][1] = son[x][1],fa[son[rt][1]] = rt,fa[rt] = 0;
rk[rt] = lk[son[rt][1]] = GetSlp (rt,son[rt][1]);
}
}
signed main(){
scanf ("%d%lf",&n,&dp[0]);
for (Int i = 1;i <= n;++ i){
scanf ("%lf%lf%lf",&A[i],&B[i],&R[i]);int best = find (rt,-A[i] / B[i]);
dp[i] = max (dp[i - 1],X[best] * A[i] + Y[best] * B[i]);
Y[i] = dp[i] / (A[i] * R[i] + B[i]),X[i] = Y[i] * R[i];
ins (rt,0,i),Rebuild (i);
}
printf ("%.3f\n",dp[n]);
return 0;
}
updated on 2020-07-15:
其实上面的代码是有问题。我们在删去凸包里面的点的时候有可能删掉不该删的点,比如:
我们在删除掉\(\texttt {H}\)的时候会顺带删掉原凸包上的\(\texttt{C}\),但这显然不合法。但是这道题的数据里面根本就没有点在凸包里面的情况,所以还是草草过掉了。