【题解】P4027 货币兑换(NOI2007)(动态维护凸包,斜率优化)
Des
小 Y 最近在一家金券交易所工作。该金券交易所只发行交易两种金券:A 纪念券(以下简称 A 券)和 B 纪念券(以下简称 B 券)。每个持有金券的顾客都有一个自己的帐户。金券的数目可以是一个实数。
每天随着市场的起伏波动,两种金券都有自己当时的价值,即每一单位金券当天可以兑换的人民币数目。我们记录第 \(K\) 天中 A 券和 B 券的价值分别为 \(A_K\) 和 \(B_K\) (元/单位金券)。
为了方便顾客,金券交易所提供了一种非常方便的交易方式:比例交易法。
比例交易法分为两个方面:
a) 卖出金券:顾客提供一个 \([0,100]\) 内的实数 \(OP\) 作为卖出比例,其意义为:将 \(OP\%\) 的 A 券和 \(OP\%\) 的 B 券以当时的价值兑换为人民币;
b) 买入金券:顾客支付 \(IP\) 元人民币,交易所将会兑换给用户总价值为 \(IP\) 的金券,并且,满足提供给顾客的 A 券和 B 券的比例在第 \(K\) 天恰好为 \(\mathrm{Rate}_ K\);
例如,假定接下来 \(3\) 天内的 \(A_K,B_K,\mathrm{Rate}_ K\) 的变化分别为:
时间 | \(A_K\) | \(B_K\) | \(\mathrm{Rate}_ K\) |
---|---|---|---|
第一天 | \(1\) | \(1\) | \(1\) |
第二天 | \(1\) | \(2\) | \(2\) |
第三天 | \(2\) | \(2\) | \(3\) |
假定在第一天时,用户手中有 \(100\) 元人民币但是没有任何金券。
用户可以执行以下的操作:
时间 | 用户操作 | 人民币(元) | A 券的数量 | B 券的数量 |
---|---|---|---|---|
开户 | 无 | \(100\) | \(0\) | \(0\) |
第一天 | 买入 \(100\) 元 | \(0\) | \(50\) | \(50\) |
第二天 | 卖出 \(50\%\) | \(75\) | \(25\) | \(25\) |
第二天 | 买入 \(60\) 元 | \(15\) | \(55\) | \(40\) |
第三天 | 卖出 \(100\%\) | \(205\) | \(0\) | \(0\) |
注意到,同一天内可以进行多次操作。
小 Y 是一个很有经济头脑的员工,通过较长时间的运作和行情测算,他已经知道了未来 \(N\) 天内的 A 券和 B 券的价值以及 \(\mathrm{Rate}\)。他还希望能够计算出来,如果开始时拥有 \(S\) 元钱,那么 \(N\) 天后最多能够获得多少元钱。
\(\texttt{Data Range:}\)
对于 \(60\%\) 的测试数据,满足 \(N \le 1 000\)。
对于 \(100\%\) 的测试数据,满足 \(N \le 10^5\)。
对于 \(100\%\) 的测试数据,满足:
\(0 < A_K \leq 10\),\(0 < B_K\le 10\),\(0 < \mathrm{Rate}_K \le 100\),\(\mathrm{MaxProfit} \leq 10^9\)。
Sol
题面居然提示你「必然存在一种最优的买卖方案满足:每次买进操作使用完所有的人民币,每次卖出操作卖出所有的金券。」,不过这个仔细一想也很对。
先来分析一点东西(下面的题解里 \(a,b\) 表示题面中的 \(A,B\))。设 \(x(ra_i+b_i)=IP\),那么用 \(IP\) 单位的钱买金券,得到的 A 卷数量为 \(xr\),B 卷数量为 \(x\). 那么如果重新卖出,得到的钱也是 \(xra_i+xb_i\),也就是说同一天内多次全部买入、全部卖出,收益是不会变多的。这个好像是废话但是得考虑一下。
那么要在第 \(i\) 天得到最多的钱,必然是在第 \(j< i\) 天全部买入了金券,在第 \(i\) 天全部卖出。设第 \(j\) 天最多的金券数量为 \(A_j,B_j\),有 \(f_i=\max_{j=1}^i (A_ja_i+B_jb_i)\).
而 \(A_i=\frac{r_if_i}{r_ia_i+b_i},B_i=\frac{f_i}{r_ia_i+b_i}\),这样这道题就能拿到 60 分了。
然后试图转化成斜率优化的形式,但这道题的式子是 \(-B_jb_i=-f_i +A_ja_i\),左边居然还有一个和 \(i\) 有关的 \(b_i\),我蒙蔽了。
看了题解才知道把 \(b_i\) 除到右边,得到
这样要让 \(f_i\) 最大,仍然是让截距最小即可。维护下凸包然后用直线去卡凸包就行。
这道题的 \(x,k\) 居然都不具有单调性,需要动态维护凸包。题解里有李超线段树,Splay,CDQ 分治的做法。本来准备用 treap 写一写,顺便复习一下平衡树,但是调了很久才发现忽略了要一直删前驱后继的操作。。。
另起炉灶,看到题解里这篇 std::set 做法 和搜到的 掌握用 STL 中的 SET 动态维护 “各类型凸壳” / “凸包”,于是效仿知乎 DALAO 的做法,继承 std::set
,封装一个 hull
类来以绝后患。就有了下面的代码。
My code
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
int n;
double f[N], a[N], b[N], r[N], A[N], B[N];
struct node {
int id, flag; // flag 记录是否用 k 来比较
double x, y, k;
bool operator<(const node &b) const {
if(flag || b.flag) { return k < b.k; }
else return x < b.x || (x == b.x && y < b.y);
}
};
inline double K(node &a, node &b) { return (a.y - b.y) / (a.x - b.x); }
struct hull : public multiset<node> { // 用 multiset 插入一模一样的点才会正确,用 set 的话删点的时候会把两个都删掉
double K(const iterator &a, const iterator &b) { return (a->y - b->y) / (a->x - b->x); }
bool inside(iterator p) {
if(p == begin()) return false;
auto t1 = prev(p), t2 = next(p);
if(t1->x == p->x) return true;
if(t2 == end()) return false;
return K(t1, p) > K(p, t2);
}
iterator cge(iterator p, double k) {
auto t = insert(node{p->id, p->flag, p->x, p->y, k});
erase(p);
return t;
}
void ins(const node &p) { // 插入一个点
auto t = insert(p);
if(inside(t)) { erase(t); return; }
while(t != begin() && inside(prev(t))) erase(prev(t));
while(next(t) != end() && inside(next(t))) erase(next(t));
if(t != begin()) {
if(prev(t) == begin()) cge(prev(t), numeric_limits<double>::min());
t = cge(t, K(prev(t), t));
} else t = cge(t, numeric_limits<double>::min());
if(next(t) != end()) cge(next(t), K(t, next(t)));
}
int find(double k) { // 用一个斜率卡下凸包,返回被卡住的点的 id
if(empty()) return 0; // 注意这个地方,看到底需要什么,这道题弄成 0 没问题
else return (--lower_bound(node{0, 1, 0, 0, k}))->id;
}
} s;
int main() {
cin >> n >> f[0];
for(int i = 1; i <= n; i++) cin >> a[i] >> b[i] >> r[i];
for(int i = 1; i <= n; i++) {
f[i] = f[i - 1];
int j = s.find(a[i] / b[i]);
f[i] = max(f[i], A[j] * a[i] + B[j] * b[i]);
double x = f[i] / (r[i] * a[i] + b[i]);
A[i] = x * r[i], B[i] = x;
s.ins(node{i, 0, A[i], -B[i], 0});
}
cout << fixed << setprecision(3) << f[n] << '\n';
return 0;
}