【题解】洛谷P4027 [NOI2007]货币兑换 斜率优化+平衡树维护凸包

原题面推荐看LOJ版本,洛谷这道题的排版布星。

在一个股市交易所中,有A,B两种金券。

已知未来N(1e5)天内每天三个实参数:A单价 p p p,B单价 q q q,比例 r r r

初始有S元人民币,每时每刻都可以进行以下操作,求N天后最多的钱数:

  1. 卖出:选择一个[0,100]内的实数OP,把两种金券都按当天价格卖出总额的OP%,获得人民币。
  2. 买入:选择一个实数IP,按当天价格买入IP元人民币的金券,其中A和B的比值等于当天的 r r r

答案不会超过1e9,数据保证计算精度误差不会超过1e-7,答案要求精度误差不超过1e-3。

提示:必然存在一种最优的方案满足,每次买进操作用完所有人民币,每次卖出操作卖出所有金券。


这个提示是NOI赛场上自带的吗,倒是把问题化简了好多。

d i d_i di表示第 i i i天只有人民币没有股票时可以获得的最大钱数。 d 0 = 0 d_0=0 d0=0

d i = m a x ( d i − 1 , m a x { d j ∗ d e a l ( j , i ) } ) , 1 < = j < i d_i = max(d_{i-1},max\{d_{j}*deal(j,i)\}),1<=j<i di=max(di1,max{djdeal(j,i)})1<=j<i

其中 d e a l ( j , i ) deal(j,i) deal(j,i)表示在在第 j j j天全部买入、第 i i i天全部卖出的情况下,总钱数的增长率。

设第 j j j天开始的时候我有 r j p j + q j r_jp_j+q_j rjpj+qj元人民币,恰好能买到 r j r_j rj A A A 1 1 1 B B B,在第 i i i天卖出就可以得到 r j p i + q i r_jp_i+q_i rjpi+qi,所以 d e a l ( i , j ) = r j p i + q i r j p j + q j deal(i,j)=\frac{r_jp_i+q_i}{r_jp_j+q_j} deal(i,j)=rjpj+qjrjpi+qi.

所以转移方程是:
$ d i = m a x ( d i − 1 , m a x { d j ( r j p i + q i ) r j p j + q j } ) , 1 < = j < = i d_i = max(d_{i-1},max\{\frac{d_{j}(r_jp_i+q_i)}{r_jp_j+q_j}\}),1<=j<=i di=max(di1,max{rjpj+qjdj(rjpi+qi)})1<=j<=i


这谁知道怎么优化啊,n^2拿60算了
住嘴你是ACM选手,有个屁的部分分

a < b < i a<b<i a<b<iTAG1)并且从 b b b转移要比从 a a a转移更优,那么

d b ( r b p i + q i ) r b p b + q b > d a ( r a p i + q i ) r a p a + q a \frac{d_{b}(r_bp_i+q_i)}{r_bp_b+q_b}>\frac{d_{a}(r_ap_i+q_i)}{r_ap_a+q_a} rbpb+qbdb(rbpi+qi)>rapa+qada(rapi+qi)

为了提出含 i i i的项,设
y i = d i r i r i p i + q i , x i = d i r i p i + q i y_i=\frac{d_{i}r_i}{r_ip_i+q_i},x_i=\frac{d_{i}}{r_ip_i+q_i} yi=ripi+qidiri,xi=ripi+qidi

然后整理得
( y b − y a ) p i + ( x b − x a ) q i > 0 (y_b-y_a)p_i+(x_b-x_a)q_i>0 (ybya)pi+(xbxa)qi>0

如果 x b − x a > 0 x_b-x_a>0 xbxa>0,我们可以得到

y b − y a x b − x a > − q i p i \frac{y_b-y_a}{x_b-x_a}>-\frac{q_i}{p_i} xbxaybya>piqi

此时由斜率优化原理,我们可以维护一个上凸包,然后对于每个目标斜率 − q i p i -\frac{q_i}{p_i} piqi,在上凸包中二分查找最优的点。

但是,由题给条件,并没有办法确定 x b − x a x_b-x_a xbxa y b − y a y_b-y_a ybya的符号。


我们把之前TAG1处的假设,由 a < b < i a<b<i a<b<i修改成 a , b < i a,b<i a,b<i a a a b b b不相等,显然上面的推导还是成立的。

现在仍然可以维护一个上凸包,只是每次添加的点并不出现在最右侧,可能出现在任意一个位置。

凸包中点的顺序一定是按 x x x排序,每次要插入一个点 ( x i , y i ) (x_i,y_i) (xi,yi)时:

  1. 二分 x i x_i xi找到待插入位置,将其加入;
  2. 以新插入的点为中心向两边排查,每形成一个下凸点,就将下凸点扔掉;
  3. 特别地,如果已有一个横坐标为 x i x_i xi的点,就扔掉坐标较小的那个,本质上还是下凸点。

要上述功能,需要用平衡树去维护凸包,查询时直接在平衡树中查询即可。


QAQ,好不想写啊。

挣扎好久,还是先写一发斜率优化,平衡树的部分被数组暴力替换了,就这也debug了好久。

/* LittleFall : Hello! */
#include <bits/stdc++.h>
using namespace std; 
const int M = 100016;
const double eps = 1e-8;
int dcmp(double a, double b)
{
	if(fabs(a-b)<eps) return 0;
	return a>b ? 1 : -1;
}

/*
所需要的操作:
1. 插入一个点,排除所有下凸点
2. 找到一个点,它右边那条线的斜率恰好小于目标斜率。
*/

double p[M], q[M], r[M];
double x[M], y[M];
double dp[M];
double slope(int i, int j) //求ij连线的斜率
{
	return (y[j]-y[i])/(x[j]-x[i]);
}

struct LifelikeBST
{
	int sz, save[M]; //实际存储范围[1, sz]
	int find(double k) //寻找斜率
	{
		int res = 1;
		while(res<sz && dcmp(slope(save[res], save[res+1]),k)>=0) ++res;
		return save[res];
	}
	void insert(int id) //插入这个点,然后删除所有下凸点
	{
		int pos = 1;
		while(pos<=sz && dcmp(x[save[pos]], x[id])<0) ++pos; //找到x第一个大于等于id的位置
		if(dcmp(x[save[pos]], x[id])==0)
		{
			if(dcmp(y[save[pos]],y[id])>=0) return;
			save[pos] = id;
		}
		else
		{
			if(pos>1 && pos<=sz && dcmp(slope(save[pos-1],id),slope(id,save[pos]))<=0)
				return;
			for(int i=sz; i>=pos; --i)
				save[i+1] = save[i];
			++sz;
			save[pos] = id;
		}
		int lp=pos-1, rp=pos+1; //左边,右边的第一个有效位置
		while(rp<sz && dcmp(slope(save[pos], save[rp]), slope(save[rp],save[rp+1]))<=0)
			++rp;
		while(lp>1 && dcmp(slope(save[lp-1], save[lp]), slope(save[lp],save[pos]))<=0)
			--lp;
		//移除(lp,pos),(pos,rp)
		int nsz = 0;
		for(int i=1; i<=sz; ++i)
			if(i<=lp || i==pos || i>=rp)
				save[++nsz] = save[i];
		sz = nsz;
	}
}qaq;

int main(void)
{
	#ifdef _LITTLEFALL_
	freopen("in.txt","r",stdin);
    #endif
	ios::sync_with_stdio(false); cin.tie(0);

	int n; cin>>n>>dp[1];
	for(int i=1; i<=n; ++i)
		cin >> p[i] >> q[i] >> r[i];

	for(int i=1; i<=n; ++i)
	{
		int j = i==1 ? 1 : qaq.find(-q[i]/p[i]);
		dp[i] = max(dp[i-1], dp[j]*(r[j]*p[i]+q[i])/(r[j]*p[j]+q[j]));
		x[i] = dp[i]/(r[i]*p[i]+q[i]), y[i] = x[i]*r[i];
		qaq.insert(i);
	}
	printf("%.3f\n",dp[n] );


    return 0;
}


inline int read(){
    int x=0,f=1;char ch=getchar();
    while(ch<'0'||ch>'9') {if(ch=='-')f=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
    return x*f;
}

这个做法能拿到70分。


然后现在就剩下把上面的迫真平衡树改成真的了,最好能总结个板子。

一共有两种大操作:插入节点,查找斜率

由迫真平衡树,向凸包中插入点 i i i分为以下几步:

  1. 找到第一个大于等于 x i x_i xi的位置 p p p,设这里对应凸包中的点 t p t_p tp
  2. 如果 x t p x_{t_p} xtp x i x_i xi相等,需要先判断它们 y y y的大小关系
    1. 如果 y t p > = y i y_{t_p}>=y_i ytp>=yi,说明 i i i在一定凸包内部,必然形成下凸点,可以扔掉,结束。
    2. 否则,删除 t p t_p tp,加入节点 i i i
  3. 如果 x t p x_{t_p} xtp x i x_i xi不相等
    1. 如果 t p − 1 t_{p-1} tp1 i i i t p t_p tp构成下凸关系,就扔掉 i i i,结束。
    2. 否则,加入节点 i i i
  4. 现在已经把节点 i i i插入在 p p p位置。考察位置 r p = p + 1 rp=p+1 rp=p+1,如果 t p , t r p , t r p + 1 t_p,t_{rp},t_{rp+1} tp,trp,trp+1构成下凸,就删去 t r p t_{rp} trp,然后给 r p + 1 rp+1 rp+1,一直重复直到没有下凸或到边界为止。
  5. 对于左边的位置 l p = p − 1 lp=p-1 lp=p1,同样进行上述操作,每次删去凸点后给 l p − 1 lp-1 lp1

查找斜率时,不建议再次二分,而是直接用平衡树自带的二分性质。

为此,我们最好把每个点右边线段的斜率也直接用平衡树维护。

插入过程中哪些斜率发生了变化?

维护nm,我心态炸了,写 l o g 2 log^2 log2

查找斜率时,需要找到一个节点,它右边那条线的斜率恰好小于目标斜率。

我就不在树上写二分了,直接写在外部,倒也好写。

写吧。


总复杂度 O ( n log ⁡ 2 n ) O(n\log^2n) O(nlog2n),带着氧气卡过去了emmm,太真实了,把这道题目总结成一个模板吧。

对了,好像有整体二分的写法,代码很简洁,之后学一下。

/* LittleFall : Hello! */
#include <bits/stdc++.h>
using namespace std; 
const int M = 100016;
const double eps = 1e-8;
int dcmp(double a, double b)
{
	if(fabs(a-b)<eps) return 0;
	return a>b ? 1 : -1;
}
template <typename T>
class Treap
{
	int l[M], r[M], p[M], m[M], b[M];
	T v[M];
    int root,id,sz;
    void lturn(int &rt) //左旋
    {
        int nrt = r[rt];
        r[rt] = l[nrt];
        l[nrt] = rt;

        m[nrt] = m[rt];
        m[rt] = m[l[rt]] + m[r[rt]] + b[rt];
        rt = nrt;
    }
    void rturn(int &rt)
    {
        int nrt = l[rt];
        l[rt] = r[nrt];
        r[nrt] = rt;

        m[nrt] = m[rt];
        m[rt] = m[l[rt]] + m[r[rt]] + b[rt];
        rt = nrt;
    }
    void insert(int &rt , T num)
    {
        if(rt == 0)
        {
            id++;
            rt = id;
            v[rt] = num;
            m[rt] = b[rt] = 1;
            p[rt] = rand();
            return;
        }
        m[rt]++;
        if(num == v[rt])
            b[rt]++;
        else if(num > v[rt])
        {
            insert(r[rt], num);
            if(p[r[rt]] > p[rt]) lturn(rt);
        }
        else
        {
            insert(l[rt], num);
            if(p[l[rt]] > p[rt]) rturn(rt);
        }
    }
    void del(int &rt, T num)
    {
        if(rt == 0) return;
        if(v[rt] == num)
        {
            if(b[rt] > 1)
                b[rt]--, m[rt]--;
            else if(l[rt] * r[rt] == 0)
                rt = l[rt] + r[rt];
            else if(p[l[rt]] < p[r[rt]])
                lturn(rt), del(rt, num);
            else
                rturn(rt), del(rt, num);
        }
        else if(num > v[rt])
            m[rt]--, del(r[rt], num);
        else
            m[rt]--, del(l[rt], num);
    }
    int lower_bound(int rt, T num)
    {
        if(rt == 0) return 1;
        if(num == v[rt])
            return m[l[rt]] + 1;
        else if(num > v[rt])
            return m[l[rt]] + b[rt] + lower_bound(r[rt], num);
        return lower_bound(l[rt], num);
    }
    T at(int rt, int rank)
    {
        if(rt == 0) return T();
        if(rank <= m[l[rt]])
            return at(l[rt], rank);
        rank -= m[l[rt]];
        if(rank <= b[rt])
            return v[rt];
        return at(r[rt], rank - b[rt]);
    }
public: 
    Treap(){root = id = sz = 0;}
    inline void insert(T num){sz++;return insert(root, num);}
    inline void del(T num){sz--;return del(root, num);}
    inline int lower_bound(T num){return lower_bound(root, num);}
    inline int upper_bound(T num){return lower_bound(root, num+1);}
    inline T at(int pos){return at(root, pos);}
    inline int size(){return sz;}
};
/*
所需要的操作:
1. 插入一个点,排除所有下凸点
2. 找到一个点,它右边那条线的斜率恰好小于目标斜率。
*/

Treap<pair<double,int>> tr;

double p[M], q[M], r[M];
double x[M], y[M];
double dp[M];
inline double slope(int i, int j) //求ij连线的斜率
{
	return (y[j]-y[i])/(x[j]-x[i]);
}

inline double cal_slope(int rk) //给出一个排名,求treap中这个点的斜率
{
	if(rk==tr.size()) return -1e18;
	return slope(tr.at(rk).second, tr.at(rk+1).second);
}
int find(double k)
{
	int lef = 1, rig = tr.size(), ans = rig;
	while(lef<=rig)
	{
		int mid = (lef+rig)>>1;
		if(dcmp(cal_slope(mid),k)<0)
		{
			ans = mid;
			rig = mid - 1;
		}
		else
		{
			lef = mid + 1;
		}
	}
	return tr.at(ans).second;
}
void insert(int id)
{
	int p = tr.lower_bound({x[id],0}); //待插入的排名
	if(p<=tr.size() && dcmp(tr.at(p).first,x[id])==0)
	{
		if(dcmp(y[tr.at(p).second],y[id])>=0) return;
		tr.del(tr.at(p));
		tr.insert({x[id],id});
	}
	else
	{
		tr.insert({x[id],id});
		if(p>1 && p<=tr.size() && dcmp(cal_slope(p-1), cal_slope(p))<=0)
		{
			tr.del(tr.at(p));
			return;
		}
	}
	while(p<=tr.size()-2 && dcmp(cal_slope(p), cal_slope(p+1))<=0)
		tr.del(tr.at(p+1));
	while(p>=3 && dcmp(cal_slope(p-2), cal_slope(p-1))<=0)
		tr.del(tr.at(--p));
	// printf("%d\n",tr.size() );
	// for(int i=1; i<=tr.size(); ++i)
	// {
	// 	printf("%d %d %.3f %.3f %.3f\n", i, tr.at(i).second,
	// 	 x[tr.at(i).second], y[tr.at(i).second],cal_slope(i) );
	// }
	// printf("\n");
}

int main(void)
{
	#ifdef _LITTLEFALL_
	freopen("in.txt","r",stdin);
    #endif
	ios::sync_with_stdio(false); cin.tie(0);

	int n; cin>>n>>dp[1];
	for(int i=1; i<=n; ++i)
		cin >> p[i] >> q[i] >> r[i];

	for(int i=1; i<=n; ++i)
	{
		int j = i==1 ? 1 : find(-q[i]/p[i]);
		dp[i] = max(dp[i-1], dp[j]*(r[j]*p[i]+q[i])/(r[j]*p[j]+q[j]));
		x[i] = dp[i]/(r[i]*p[i]+q[i]), y[i] = x[i]*r[i];
		insert(i);
	}
	printf("%.3f\n",dp[n] );


    return 0;
}


inline int read(){
    int x=0,f=1;char ch=getchar();
    while(ch<'0'||ch>'9') {if(ch=='-')f=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
    return x*f;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值