[BZOJ]1492 货币兑换Cash 斜率优化 动态维护凸包

1492: [NOI2007]货币兑换Cash

Time Limit: 5 Sec   Memory Limit: 64 MB
Submit: 5690   Solved: 2289
[ Submit][ Status][ Discuss]

Description

小Y最近在一家金券交易所工作。该金券交易所只发行交易两种金券:A纪念券(以下简称A券)和 B纪念券(以下
简称B券)。每个持有金券的顾客都有一个自己的帐户。金券的数目可以是一个实数。每天随着市场的起伏波动,
两种金券都有自己当时的价值,即每一单位金券当天可以兑换的人民币数目。我们记录第 K 天中 A券 和 B券 的
价值分别为 AK 和 BK(元/单位金券)。为了方便顾客,金券交易所提供了一种非常方便的交易方式:比例交易法
。比例交易法分为两个方面:(a)卖出金券:顾客提供一个 [0,100] 内的实数 OP 作为卖出比例,其意义为:将
 OP% 的 A券和 OP% 的 B券 以当时的价值兑换为人民币;(b)买入金券:顾客支付 IP 元人民币,交易所将会兑
换给用户总价值为 IP 的金券,并且,满足提供给顾客的A券和B券的比例在第 K 天恰好为 RateK;例如,假定接
下来 3 天内的 Ak、Bk、RateK 的变化分别为:
假定在第一天时,用户手中有 100元 人民币但是没有任何金券。用户可以执行以下的操作:
注意到,同一天内可以进行多次操作。小Y是一个很有经济头脑的员工,通过较长时间的运作和行情测算,他已经
知道了未来N天内的A券和B券的价值以及Rate。他还希望能够计算出来,如果开始时拥有S元钱,那么N天后最多能
够获得多少元钱。

Input

输入第一行两个正整数N、S,分别表示小Y能预知的天数以及初始时拥有的钱数。接下来N行,第K行三个实数AK、B
K、RateK,意义如题目中所述。对于100%的测试数据,满足:0<AK≤10;0<BK≤10;0<RateK≤100;MaxProfit≤1
0^9。
【提示】
1.输入文件可能很大,请采用快速的读入方式。
2.必然存在一种最优的买卖方案满足:
每次买进操作使用完所有的人民币;
每次卖出操作卖出所有的金券。

Output

只有一个实数MaxProfit,表示第N天的操作结束时能够获得的最大的金钱数目。答案保留3位小数。

Sample Input

3 100
1 1 1
1 2 2
2 2 3

Sample Output

225.000

HINT

Source

[ Submit][ Status][ Discuss]


HOME Back

  现在才知道斜率优化可以理解为截距最大化, 太弱辣... 以前都是用不等式来推导的, 发现用截距最大化来理解很神...

  这道题很明显, 长得就像一个dp. 每天可以多次操作, 但是我们想啊, 如果说要买, 那肯定是买了优才买, 那我还不如全买, 卖也是同理. 那么仔细想想到了第i天的最优策略肯定是在第j天把钱全部换成A和B,然后在这一天全部卖掉. 当然也有可能第i天不买不卖, 那么就延续i-1的状态就好了. 我们设f[i]表示到了第i天的时候我最多能赚多少钱. 那么第i天最多能换多少A多少B呢. 假设B可换y张,那么A就可以换到rate[i] * y张. 那么就得到y * b[i] + rate[i] * y * a[i] = f[i], 然后化一下式子就会发现y = f[i] / (a[i] * r[i] + b[i]), 也就是最多换的B, 同理乘上rate[i]就可以得到A的数量. 那么f[i]的转移方程也就是呼之欲出(r[j]就是rate[j]):

  f[i] = max(f[i - 1], ( (f[j] * r[j])  /  (a[j] * r[j] + b[j]))  *  a[i] + (f[j] / (a[j] * r[j] + b[j])) * b[i]) ).

  然后先不看f[i - 1], 我们推导一下原式发现令x[j] = (f[j] * r[j])  /  (a[j] * r[j] + b[j])) , y[j] = (f[j] / (a[j] * r[j] + b[j])). 那么就是f[i] = x[j] * a[i] + y[j] * b[i], 想办法把所求的变成截距就化成了:

  y[j] = - (a[i] / b[i]) * x[j] + f[i] / b[i]. 那么把所有的x[j]想成二维坐标系x坐标, y[j]想成y坐标. 我们会发现取max在这里就是使截距最大化. 每个x[j], y[j]就是一个点. 由于截距要最大, 那么最优j一定是在凸壳上. 我们的-a[i]/b[i]就像当时于是用这个斜率去切这个凸壳. 显然这是一个上凸壳, 截距最大的时候就是斜率穿过某点时所有点均在斜率直线右侧. 此时这个点就是最优点j. 它与凸壳上左右构成的斜率一个大于-a[i]/b[i], 一个小于. 由于凸壳上斜率单调我们二分一下就能知道最优点在哪里. 但是由于每次的x[i]不是单增的, 那么就涉及到在原来凸壳上插入的情况. 那么就用Splay以x[i]为关键字动态维护这个凸壳就可以了. 具体直接可以维护凸壳每个点和左右点构成的斜率lk[x]和rk[x]. 插入一个点的时候分别向这个点x左右删点. 删点的时候, 由于splay里x单增且又维护的是上凸壳, 所以斜率单减. 那么我们靠lk和rk就能分别在左右找到能与当前插入点x构成新的凸包的点, 中间的删去即可——这也就是splay来维护的好处, 直接区间提取一起删除.

  发现splay越来越好写了啊 , 1A

#include<bits/stdc++.h>
using namespace std;
const int inf = 1e9;
const int maxn = 2e5 + 5;
const double eps = 1e-10;
int n, rt;
int c[maxn][2], fa[maxn];
double lk[maxn], rk[maxn], y[maxn], x[maxn], f[maxn], a[maxn], b[maxn], r[maxn];
inline double getk(int i, int j) {
	if (fabs(x[j] - x[i]) < eps) return -inf;
	return (y[j] - y[i]) / (x[j] - x[i]);
}
inline void rotate(int x, int &wanna) {
	int y = fa[x], z = fa[y];
	int l = (c[y][1] == x), r = l ^ 1;
	if (y ^ wanna) c[z][c[z][1] == y] = x;
	else wanna = x;
	fa[x] = z, fa[y] = x, fa[c[x][r]] = y;
	c[y][l] = c[x][r], c[x][r] = y;
}
inline void splay(int x, int &wanna) {
	for (int f; x ^ wanna; rotate(x, wanna))
		if ((f = fa[x]) ^ wanna)
			rotate((c[fa[f]][0] == f ^ c[f][0] == x) ? x : f, wanna);
}
void insert(int &k, int nx, int f) {
	if (!k) {
		k = nx, fa[k] = f;
		splay(nx, rt);
		return;
	}
	if (x[nx] <= x[k] + eps) insert(c[k][0], nx, k);
	else insert(c[k][1], nx, k);
}
void erase(int x) {
	splay(x, rt);
	rt = c[x][1], c[rt][0] = c[x][0], fa[rt] = 0;
	fa[c[rt][0]] = rt;
	lk[rt] = rk[c[rt][0]] = getk(c[rt][0], rt);
}
inline int prev(int x) {
	int k = c[x][0], tmp = x;
	while (k) {
		if (getk(k, x) <= lk[k] + eps) tmp = k, k = c[k][1];
		else k = c[k][0];
	}
	return tmp;
}
inline int succ(int x) {
	int k = c[x][1], tmp = x;
	while (k) {
		if (getk(x, k) + eps >= rk[k]) tmp = k, k = c[k][0];
		else k = c[k][1];
	}
	return tmp;
}
inline void push_in(int x) {
	insert(rt, x, 0);
	if (c[x][0]) {
		int lf = prev(x);
		splay(lf, c[x][0]), c[lf][1] = 0;
		lk[x] = rk[lf] = getk(lf, x);
	}
	else lk[x] = inf;
	if (c[x][1]) {
		int rg = succ(x);
		splay(rg, c[x][1]), c[rg][0] = 0;
		rk[x] = lk[rg] = getk(x, rg);
	}
	else rk[x] = -inf;
	if (lk[x] <= rk[x] + eps) erase(x);
}
int ffind(int x, double k) {
	if (!x) return 0;
	if (lk[x] + eps >= k && k + eps >= rk[x]) return x;
	else if (lk[x] + eps >= k) return ffind(c[x][1], k);
	else ffind(c[x][0], k);
}
int main() {
	scanf("%d%lf", &n, &f[0]);
	register int i;
	for (i = 1; i <= n; ++ i) scanf("%lf%lf%lf", &a[i], &b[i], &r[i]);
	for (i = 1; i <= n; ++ i) {
		int j = ffind(rt, -a[i] / b[i]);
		f[i] = max(f[i - 1], x[j] * a[i] + y[j] * b[i]);
		y[i] = f[i] / (a[i] * r[i] + b[i]);
		x[i] = r[i] * y[i];
		push_in(i);
	}
	printf("%.3lf\n", f[n]);
}



  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值