• 【题解】P4027 货币兑换(NOI2007)(动态维护凸包,斜率优化)



    time: 2021-11-17 19:10:44
    tags:

    • 题解 斜率优化DP NOI 计算几何/凸包

    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\) 除到右边,得到

    \[-B_j=A_j\frac{a_i}{b_i}-\frac{f_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;
    }
    
  • 相关阅读:
    介绍一种很好用的任务调度平台
    java中的进制与操作符
    类再生(合成、继承、final)
    初始化
    重新学习Spring2——IOC和AOP原理彻底搞懂
    重新学习Spring一--Spring在web项目中的启动过程
    JDK并发包
    java并行程序基础
    MVC模式
    访问者模式
  • 原文地址:https://www.cnblogs.com/huaruoji/p/treapwdnmd.html
Copyright © 2020-2023  润新知