P4027 [NOI2007] 货币兑换
[NOI2007] 货币兑换
题目描述
小 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\) 天后最多能够获得多少元钱。
输入格式
第一行两个正整数 \(N,S\),分别表示小 Y 能预知的天数以及初始时拥有的钱数。
接下来 \(N\) 行,第 \(K\) 行三个实数 \(A_K,B_K,\mathrm{Rate} _ K\) ,意义如题目中所述。
输出格式
只有一个实数 \(\mathrm{MaxProfit}\),表示第 \(N\) 天的操作结束时能够获得的最大的金钱数目。答案保留 \(3\) 位小数。
样例 #1
样例输入 #1
3 100
1 1 1
1 2 2
2 2 3
样例输出 #1
225.000
提示
时间 | 用户操作 | 人民币(元) | A 券的数量 | B 券的数量 |
---|---|---|---|---|
开户 | 无 | \(100\) | \(0\) | \(0\) |
第一天 | 买入 \(100\) 元 | \(0\) | \(50\) | \(50\) |
第二天 | 卖出 \(100\%\) | \(150\) | \(0\) | \(0\) |
第二天 | 买入 \(150\) 元 | \(0\) | \(75\) | \(37.5\) |
第三天 | 卖出 \(100\%\) | \(225\) | \(0\) | \(0\) |
本题没有部分分,你的程序的输出只有和标准答案相差不超过 \(0.001\) 时,才能获得该测试点的满分,否则不得分。
测试数据设计使得精度误差不会超过 \(10^{-7}\) 。
对于 \(40\%\) 的测试数据,满足 \(N \le 10\)。
对于 \(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\)。
输入文件可能很大,请采用快速的读入方式。
必然存在一种最优的买卖方案满足:
每次买进操作使用完所有的人民币,每次卖出操作卖出所有的金券。
Solution
假设一天的钱是 \(f\),那么设 \(x_i\) 表示第 \(i\) 天买的 \(A\) 类的数量,\(y_i\) 表示 \(B\) 类,那么有:
那么有:
这样就有了一个 \(\mathcal O(n^2)\) 的 \(\texttt{DP}\)。考虑怎么优化,这很显然是一个可以斜率优化的式子。
若 \(j\) 比 \(k\) 更优,那么有:
这样就可以斜率优化了。但是非常恶心的是 \(x,k\) 都不满足单调性,所以不能够直接用单调栈或者队列维护凸壳。
一种可行的解决方式是用 CDQ 分治进行转移,先按照 \(k=-\dfrac{a_i}{b_i}\) 排序,然后:
- 若当前区间长度为 \(1\),尝试从 \(f_{i-1}\) 转移。
- 递归处理左半区间。
- 将左半区间加入单调队列,维护凸包。
- 计算左半区间对右半区间的贡献。
- 递归处理右半区间。
- 将当前区间按照 \(x\) 进行归并排序,方便之后维护单调队列。
这样这道题就解决了。代码上需要注意一些细节。如果开始对 \(k\) 排序是从小到大那么维护凸壳就应该用单调栈,反之使用单调队列。
完整代码
#include<bits/stdc++.h>
#undef DEBUG
using namespace std;
namespace Hanx16qwq {
constexpr int _SIZE = 1e5;
constexpr long double eps = 1e-18;
int n;
long double S;
struct Node {
int id;
long double x, y;
}q[_SIZE + 5], ql[_SIZE + 5], qr[_SIZE + 5];
long double A[_SIZE + 5], B[_SIZE + 5], R[_SIZE + 5];
long double f[_SIZE + 5];
int s[_SIZE + 5], head, tail;
long double px(int x) {return -A[x] / B[x];}
long double slope(int j, int i) {
return (q[i].y - q[j].y) / (q[i].x - q[j].x + eps);
}
void CDQ(int l, int r) {
if (l == r) {
f[l] = max(f[l], f[l - 1]);
q[l].x = f[l] * R[l] / (A[l] * R[l] + B[l]);
q[l].y = f[l] / (A[l] * R[l] + B[l]);
return;
}
int mid = (l + r) >> 1, lp = 0, rp = 0;
for (int i = l; i <= r; i++)
if (q[i].id <= mid) ql[++lp] = q[i];
else qr[++rp] = q[i];
for (int i = 1; i <= lp; i++) q[i + l - 1] = ql[i];
for (int i = 1; i <= rp; i++) q[mid + i] = qr[i];
CDQ(l, mid);
head = 1, tail = 0;
for (int i = l; i <= mid; i++) {
while (tail > head && slope(s[tail], i) > slope(s[tail - 1], s[tail])) tail--;
s[++tail] = i;
}
for (int I = mid + 1; I <= r; I++) {
int i = q[I].id;
while (tail > head && slope(s[head], s[head + 1]) > -(A[i] / B[i])) head++;
int j = s[head];
f[i] = max(f[i], q[j].x * A[i] + q[j].y * B[i]);
}
CDQ(mid + 1, r);
lp = l, rp = mid + 1; int p = 0;
while (lp <= mid && rp <= r)
if (q[lp].x < q[rp].x) ql[++p] = q[lp++];
else ql[++p] = q[rp++];
while (lp <= mid) ql[++p] = q[lp++];
while (rp <= r) ql[++p] = q[rp++];
for (int i = 1; i <= p; i++) q[l + i - 1] = ql[i];
}
void main() {
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin >> n >> S;
for (int i = 1; i <= n; i++) {
cin >> A[i] >> B[i] >> R[i];
q[i].id = i;
q[i].x = R[i] * S / (A[i] * R[i] + B[i]);
q[i].y = S / (A[i] * R[i] + B[i]);
f[i] = S;
}
sort(q + 1, q + n + 1, [&](Node x, Node y) {
return px(x.id) > px(y.id);
});
CDQ(1, n);
cout << fixed << setprecision(3) << f[n] << '\n';
}
}
signed main() {
#ifdef DEBUG
freopen("../test.in", "r", stdin);
freopen("../test.out", "w", stdout);
#endif
Hanx16qwq::main();
return 0;
}