P4027 [NOI2007] 货币兑换 题解
题意
你有一些钱,有两种金券(A, B)。
第 \(i\) 天,你可以如下操作:
-
按照当天价格卖出 A, B 金券各 \(k%\)。
-
按照当天价格花 \(k\) 元买金券,A, B 分别买到 \(p, q\) 个,满足 \(\frac p q = \text{Rate}_i\)。
给定 \(A_{1,\cdots,n}, B_{1,\cdots,n}, \text{Rate}_{1,\cdots,n}\),分别为每天 A, B 金券价格和购买比例。
求第 \(n\) 天最大收益。
\(n \le 10 ^ 5, A_{1,\cdots,n}, B_{1,\cdots,n} \le 10, \text{Rate}_{1,\cdots,n} \le 100\)。
思路
DP
题目提示我们每次卖都是卖完金券,买都花光钱。所以我们是交替的一次买,一次卖。
对于分界线两边:
-
左侧最后一个是买,右侧第一个是卖,需要知道左侧买了多少金券(两种,分别多少)。
又因为买的时候花光钱,我们只需要知道左侧最后一个买的位置最多有多少钱即可。 -
左侧最后一个是卖,右侧第一个是买,需要知道左侧还有多少钱。
设 \(f_i\) 表示第 \(i\) 天最多拥有的钱(这一天不买金券),\(p_i, q_i\) 分别表示拿 \(f_i\) 的钱在第 \(i\) 天能买多少 A / B 金券。
下记 \(R_i\) 为 \(\text{Rate}_i\)。
解得:
考虑转移,我们枚举 \(i\),枚举 \(j\),算出在 \(j\) 花光钱买金券,在 \(i\) 全部卖掉的钱。
斜率优化
原式子 \(f_i = A_i \times p_j + B_i \times q_j\) 中含有两个同时含有 \(i, j\) 的项。
但是我们考虑在两侧除一个 \(B_i\),得到 \(\frac {f_i} {B_i} = \frac {A_i} {B_i} \times p_j + q_j\)。
分析插入顺序和查询斜率顺序,发现两个顺序都是乱的,所以需要动态维护凸包。
Splay
我们用 Splay 维护凸包。
对于凸包上的每个点,维护其左侧直线斜率 \(lk\) 及右侧直线斜率 \(rk\)。
查询最优决策时,从根节点往下走,找到 \(lk_j \le - \frac {A_i} {B_i} \le rk_j\) 的 \(j\)。
插入新的节点时,先找到左侧最靠右的凸包上的点(这里是指加入新点后还在凸包上)和右侧最靠左的凸包上的点,以此计算新点的 \(lk, rk\)。
若新点并不会出现在凸包上,即 \(lk \lt rk\),需要删除。
代码
const int N = 1e5 + 5;
const double eps = 1e-9;
const double inf = 1e9;
int n;
double dp[N];
double a[N], b[N], r[N];
double p[N], q[N];
struct node{
int fa, son[2];
double lk, rk;
} t[N];
int tot, rt;
#define fa(x) t[x].fa
#define ls(x) t[x].son[0]
#define rs(x) t[x].son[1]
bool which_son(int x){
return x == rs(fa(x));
}
void rotate(int x){
int Fa = fa(x), GFa = fa(Fa);
int flg_x = which_son(x);
t[Fa].son[flg_x] = t[x].son[flg_x ^ 1];
t[x].son[flg_x ^ 1] = Fa;
if(GFa) t[GFa].son[which_son(Fa)] = x;
if(t[Fa].son[flg_x]) fa(t[Fa].son[flg_x]) = Fa;
fa(Fa) = x;
fa(x) = GFa;
}
void splay(int &root, int x){
int fa_root = fa(root);
for(int Fa = fa(x); (Fa = fa(x)) != fa_root; rotate(x)){
if(fa(Fa) != fa_root){
if(which_son(x) == which_son(Fa)){
rotate(Fa);
}else{
rotate(x);
}
}
}
root = x;
}
int find_dec(double qk){
int x = rt;
while(x){
if(t[x].lk >= qk && t[x].rk <= qk){
break;
}
if(t[x].lk < qk){
x = ls(x);
}else{
x = rs(x);
}
}
return x;
}
double calc_k(int i, int j){
double x_1 = p[i], y_1 = q[i];
double x_2 = p[j], y_2 = q[j];
if(fabs(x_1 - x_2) < eps){
return (y_1 < y_2) ? inf : -inf;
}
return (y_2 - y_1) / (x_2 - x_1);
}
int query_pre(int x){
int y = ls(x), res = y;
while(y){
if(t[y].lk >= calc_k(y, x)){
res = y;
y = rs(y);
}else{
y = ls(y);
}
}
return res;
}
int query_nxt(int x){
int y = rs(x), res = y;
while(y){
if(t[y].rk <= calc_k(x, y)){
res = y;
y = ls(y);
}else{
y = rs(y);
}
}
return res;
}
void insert(int i){ // insert point (p_i, q_i)
int x = rt, Fa = 0;
while(x){
if(p[x] >= p[i]){
Fa = x;
x = ls(x);
}else{
Fa = x;
x = rs(x);
}
}
t[Fa].son[p[i] > p[Fa]] = i;
fa(i) = Fa;
splay(rt, i);
}
void init(int x){
if(ls(x)){
int pre = query_pre(x);
splay(ls(rt), pre);
rs(pre) = 0;
t[pre].rk = t[x].lk = calc_k(pre, x);
}else{
t[x].lk = inf;
}
if(rs(x)){
int nxt = query_nxt(x);
splay(rs(rt), nxt);
ls(nxt) = 0;
t[nxt].lk = t[x].rk = calc_k(x, nxt);
}else{
t[x].rk = -inf;
}
if(t[x].lk <= t[x].rk + eps){
rt = ls(x), fa(rt) = 0;
rs(rt) = rs(x), fa(rs(rt)) = rt;
t[rt].rk = t[rs(rt)].lk = calc_k(rt, rs(rt));
}
}
#undef fa
#undef ls
#undef rs
void solve_test_case(){
cin >> n >> dp[0];
rep(i, 1, n){
cin >> a[i] >> b[i] >> r[i];
int j = find_dec(-a[i] / b[i]);
dp[i] = max(dp[i - 1], a[i] * p[j] + b[i] * q[j]);
p[i] = dp[i] * r[i] / (a[i] * r[i] + b[i]);
q[i] = dp[i] / (a[i] * r[i] + b[i]);
insert(i);
init(i);
}
cout << fixed << setprecision(3) << dp[n];
}

浙公网安备 33010602011771号