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\) 类,那么有:

\[x_i=\dfrac{fr_i}{a_ir_i+b_i} \]

\[y_i=\dfrac{f}{a_ir_i} \]

那么有:

\[f_i = \max\{f_{i-1}, \max\{a_ix_j+b_iy_j\}\} \]

这样就有了一个 \(\mathcal O(n^2)\)\(\texttt{DP}\)。考虑怎么优化,这很显然是一个可以斜率优化的式子。

\(j\)\(k\) 更优,那么有:

\[\begin{array}{rll} x_ja_i+y_jb_i&>&x_ka_i+y_kb_i\\ -(x_j-x_k)a_i&>&(y_j-y_k)b_i\\ -\dfrac{a_i}{b_i}&<&\dfrac{y_j-y_k}{x_j-x_k} \end{array} \]

这样就可以斜率优化了。但是非常恶心的是 \(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;
}
posted @ 2022-11-11 11:25  Hanx16Msgr  阅读(24)  评论(0编辑  收藏  举报