【BZOJ1492】【NOI2007】货币兑换(动态规划,CDQ分治,Splay)

【BZOJ1492】【NOI2007】货币兑换(动态规划,CDQ分治,Splay)

题面

BZOJ
洛谷

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 的变化分别为:

img

假定在第一天时,用户手中有 100元 人民币但是没有任何金券。用户可以执行以下的操作:

img

注意到,同一天内可以进行多次操作。小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

img

题解

首先考虑一下暴力吧

我们想想,肯定是一天把手上所有钱全部买了之后在后面的某一天把他们全部卖出去。

考虑一个\(dp\)
\(f[i]\)表示第\(i\)天手上的\(A\)券的最大值
当然,记录手中的最大钱数或者\(B\)券的数量都是一样的
\(ans\)记录任意时刻手中的最多钱数
那么,
对于一个\(f[i]\)而言
\(ans=max(ans,f[j]全部在第i天卖掉)\)
\(f[i]=ans全部买掉\)
这样的\(dp\)很显然是\(O(n^2)\)\(60pts\)

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<cmath>
#include<algorithm>
using namespace std;
#define ll long long
#define RG register
#define MAX 111111
double A[MAX],B[MAX],R[MAX];
double f[MAX],ans;
int n,S;
int main()
{
	ios::sync_with_stdio(false);
	cin>>n>>S;
	for(int i=1;i<=n;++i)cin>>A[i]>>B[i]>>R[i];
	f[1]=S*R[1]/(A[1]*R[1]+B[1]);
	ans=S;
	for(int i=2;i<=n;++i)
	{
		for(int j=1;j<i;++j)
			ans=max(ans,f[j]*A[i]+f[j]/R[j]*B[i]);
		f[i]=ans*R[i]/(A[i]*R[i]+B[i]);
	}
	printf("%.3lf\n",ans);
	return 0;
}

显然在\(dp\)的时候,枚举哪一天是少不了的
现在考虑的是快速计算\(ans\)
把关于\(ans\)的式子写一下
\(ans=max(ans,f[j]*A[i]+f[j]/R[j]*B[i])\)
假设\(j\)位置的转移比\(k\)位置更优

\[A[i]*f[j]+\frac{f[j]}{R[j]}*B[i]>A[i]*f[k]+\frac{f[k]}{R[k]}*B[i] \]

化简得到

\[-\frac{A[i]}{B[i]}>\frac{\frac{f[j]}{R[j]}-\frac{f[k]}{R[k]}}{f[j]-f[k]} \]

上面那坨东西好不爽
\(g[i]=\frac{f[i]}{R[i]}\)

\[-\frac{A[i]}{B[i]}>\frac{g[j]-g[k]}{f[j]-f[k]} \]

左边这个像个斜率的形式,右边像直线的两点式
因此,对于每一天
都可以对应一个点\((f[i],g[i])\)
以及一个斜率\(-\frac{A[i]}{B[i]}\)

维护一个凸壳??
但是\(f[i]\)显然不递增,\(g[i]\)也显然不递增
没法用单调栈来维护。。。很蛋疼啊。。。

要求的东西显然是所有的直线中,斜率小于目标\(K\)
并且连接之后与\(y\)轴的截距最大

所以,维护一个上凸壳,在里面维护斜率,
同时,支持任意插点,动态维护凸壳,二分斜率等操作
看起来是个平衡树之类的东西。。。


方法一:\(CDQ\)分治
我肯定不会闲着蛋疼先去写平衡树,
所以我们先来写一个稍微好写点的\(CDQ\)分治
可以参考\(CDQ\)的论文

其实我也是照着网上题解打的

这里稍微换一下,\(f[i]\)表示的是第\(i\)天时手上的最大钱数。
其他的转移大致相同,不再提了。
当然,坐标也有一点点小小的变化

考虑如何\(CDQ\)分治

很显然,如果我要求出\(x\)位置的值
显然就要维护出\(1..x-1\)的值
所以进行\(CDQ\)分治
第一点,肯定是按照天数排序分割左右
但是同一侧内部不一定按照天数

考虑怎么计算左右之间的贡献
相当把左侧的凸壳先构造出来,右侧按照斜率排序
用一个单调指针扫过去依次更新答案就行了

因为左侧要维护凸壳
因此归并排序的时候按照\(x\)坐标排序
每次暴力把左侧的凸壳构造出来,复杂度\(O(n)\)

当只有一个点的时候,这个点的\(dp\)值就已经算出来了
同时,\(x,y\)坐标也可以算出来

所以这样的复杂度是多少???
我觉得是\(O(nlogn)\)

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<cmath>
#include<algorithm>
using namespace std;
#define ll long long
#define RG register
#define MAX 111111
struct Node{double a,b,r,k,x,y;int id;}p[MAX],q[MAX];
bool operator<(Node a,Node b){return a.k>b.k;}
double f[MAX],ans;
int n,S[MAX],top;
double K(int x,int y)
{
	if(!y||fabs(p[x].x-p[y].x)<1e-9)return -1e20;
	return (p[x].y-p[y].y)/(p[x].x-p[y].x);
}
void CDQ(int l,int r)
{
	int mid=(l+r)>>1;
	if(l==r)
	{
		f[l]=max(f[l],f[l-1]);
		p[l].y=f[l]/(p[l].a*p[l].r+p[l].b);
		p[l].x=p[l].y*p[l].r;
		return;
	}
	int t1=l,t2=mid+1;
	for(int i=l;i<=r;++i)
		if(p[i].id<=mid)q[t1++]=p[i];
		else q[t2++]=p[i];
	for(int i=l;i<=r;++i)p[i]=q[i];
	CDQ(l,mid);top=0;
	for(int i=l;i<=mid;++i)
	{
		while(top>1&&K(S[top-1],S[top])<K(S[top-1],i))--top;
		S[++top]=i;
	}
	S[++top]=0;int nw=1;
	for(int i=mid+1;i<=r;++i)
	{
		while(nw<top&&K(S[nw],S[nw+1])>p[i].k)++nw;
		f[p[i].id]=max(f[p[i].id],p[i].a*p[S[nw]].x+p[i].b*p[S[nw]].y);
	}
	CDQ(mid+1,r);
	t1=l;t2=mid+1;
	for(int i=l;i<=r;++i)
		if(((p[t1].x<p[t2].x)||(fabs(p[t1].x-p[t2].x)<1e-9&&p[t1].y<p[t2].y)||t2>r)&&t1<=mid)
			q[i]=p[t1++];
		else q[i]=p[t2++];
	for(int i=l;i<=r;++i)p[i]=q[i];
}
int main()
{
	ios::sync_with_stdio(false);
	cin>>n>>f[0];
	for(int i=1;i<=n;++i)
	{
		cin>>p[i].a>>p[i].b>>p[i].r;
		p[i].k=-p[i].a/p[i].b;p[i].id=i;
	}
	sort(&p[1],&p[n+1]);
	CDQ(1,n);
	printf("%.3lf\n",f[n]);
	return 0;
}


方法二:平衡树
因为每个点的\(x\)坐标并不是严格递增的
因此我们需要支持动态插点,动态维护凸壳

想清楚怎么插吧。。。
假设我们已经维护了一个上图壳
如果新插的点在凸壳内部,这个点可以直接扔掉
如果在凸壳外部,那么相当于要删掉凸壳上的一段点

找到左侧第一个能够和它一起卡在凸壳上的点
找到右侧第一个能够和它一起卡在凸壳上的点
把中间的点全部删掉就行了
然后我就写死了。。。
这个貌似可以用\(set\)做吧。。。
左右暴力找就行了(因为只要找到了就可以直接删掉)
求答案的时候二分一下就行了。。。
我的STL水平显然不够啊。。。

稍微注意一个细节:这题口口声声说不卡精度,但是如果不卡\(eps\)会挂。。。。

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<set>
#include<map>
#include<vector>
#include<queue>
using namespace std;
#define ll long long
#define RG register
#define MAX 111111
#define INF 1e9
int n;
double a[MAX],b[MAX],r[MAX];
double x[MAX],y[MAX],f[MAX];
double K(int i,int j)
{
	if(fabs(x[i]-x[j])<1e-7)return -1e20;
	return (y[i]-y[j])/(x[i]-x[j]);
}
struct SplayTree
{
	int root,tot;
	struct Node
	{
		int ch[2],ff;
		double k1,k2;
	}t[MAX];
	void rotate(int x)
	{
		int y=t[x].ff,z=t[y].ff;
		int k=t[y].ch[1]==x;
		if(z)t[z].ch[t[z].ch[1]==y]=x;t[x].ff=z;
		t[y].ch[k]=t[x].ch[k^1];t[t[x].ch[k^1]].ff=y;
		t[x].ch[k^1]=y;t[y].ff=x;
	}
	void Splay(int x,int goal)
	{
		while(t[x].ff!=goal)
		{
			int y=t[x].ff,z=t[y].ff;
			if(z!=goal)(rand()&1)?rotate(x):rotate(y);
			rotate(x);
		}
		if(!goal)root=x;
	}
	int findpre(int x)
	{
		int u=t[x].ch[0],ret=u;
		while(u)
			if(K(u,x)<=t[u].k1)ret=u,u=t[u].ch[1];
			else u=t[u].ch[0];
		Splay(ret,x);
		return ret;
	}
	int findnxt(int x)
	{
		int u=t[x].ch[1],ret=u;
		while(u)
			if(K(u,x)>=t[u].k2)ret=u,u=t[u].ch[0];
			else u=t[u].ch[1];
		Splay(ret,x);
		return ret;
	}
	void insert()
	{
		int u=root,nw=++tot;
		if(!root){root=nw;return;}
		while(233)
			if(x[nw]-x[u]<=1e-7)
			{
				if(!t[u].ch[0]){t[u].ch[0]=nw;t[nw].ff=u;break;}
				else u=t[u].ch[0];
			}
			else
			{
				if(!t[u].ch[1]){t[u].ch[1]=nw;t[nw].ff=u;break;}
				else u=t[u].ch[1];
			}
	}
	void Work(int nw)
	{
		Splay(nw,0);
		if(t[nw].ch[0])
		{
			int lt=findpre(nw);t[t[lt].ch[1]].ff=0;t[lt].ch[1]=0;
			t[lt].k2=t[nw].k1=K(lt,nw);
		}
		else t[nw].k1=INF;
		if(t[nw].ch[1])
		{
			int rg=findnxt(nw);t[t[rg].ch[0]].ff=0;t[rg].ch[0]=0;
			t[rg].k1=t[nw].k2=K(nw,rg);
		}
		else t[nw].k2=-INF;
		if(t[nw].k1<=t[nw].k2)
		{
			t[root=t[nw].ch[0]].ff=0,t[t[root].ch[1]=t[nw].ch[1]].ff=root;
			t[root].k2=t[t[nw].ch[1]].k1=K(t[nw].ch[1],root);
		}
	}
	int Query(double K)
	{
		int u=root;
		while(233)
		{
			if(!u)return 0;
			if(t[u].k2<=K&&t[u].k1>=K)return u;
			if(t[u].k1<K)u=t[u].ch[0];
			else u=t[u].ch[1];
		}
	}
}Splay;
int main()
{
	scanf("%d%lf",&n,&f[0]);
	for(int i=1;i<=n;++i)scanf("%lf%lf%lf",&a[i],&b[i],&r[i]);
	for(int i=1;i<=n;++i)
	{
		int j=Splay.Query(-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]=y[i]*r[i];
		Splay.insert();Splay.Work(i);
	}
	printf("%.3lf\n",f[n]);
	return 0;
}

posted @ 2018-04-04 14:46  小蒟蒻yyb  阅读(495)  评论(1编辑  收藏  举报