滑蒻稽的博客

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

\[-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;
}
posted @ 2021-11-17 19:34  huaruoji  阅读(74)  评论(0编辑  收藏  举报