原题面推荐看LOJ版本,洛谷这道题的排版布星。
在一个股市交易所中,有A,B两种金券。
已知未来N(1e5)天内每天三个实参数:A单价 p p p,B单价 q q q,比例 r r r。
初始有S元人民币,每时每刻都可以进行以下操作,求N天后最多的钱数:
- 卖出:选择一个[0,100]内的实数OP,把两种金券都按当天价格卖出总额的OP%,获得人民币。
- 买入:选择一个实数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(di−1,max{dj∗deal(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(di−1,max{rjpj+qjdj(rjpi+qi)}),1<=j<=i
这谁知道怎么优化啊,n^2拿60算了
住嘴你是ACM选手,有个屁的部分分
设 a < b < i a<b<i a<b<i(TAG1)并且从 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
(yb−ya)pi+(xb−xa)qi>0
如果 x b − x a > 0 x_b-x_a>0 xb−xa>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} xb−xayb−ya>−piqi
此时由斜率优化原理,我们可以维护一个上凸包,然后对于每个目标斜率 − q i p i -\frac{q_i}{p_i} −piqi,在上凸包中二分查找最优的点。
但是,由题给条件,并没有办法确定 x b − x a x_b-x_a xb−xa或 y b − y a y_b-y_a yb−ya的符号。
我们把之前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)时:
- 二分 x i x_i xi找到待插入位置,将其加入;
- 以新插入的点为中心向两边排查,每形成一个下凸点,就将下凸点扔掉;
- 特别地,如果已有一个横坐标为 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分为以下几步:
- 找到第一个大于等于 x i x_i xi的位置 p p p,设这里对应凸包中的点 t p t_p tp
- 如果
x
t
p
x_{t_p}
xtp和
x
i
x_i
xi相等,需要先判断它们
y
y
y的大小关系
- 如果 y t p > = y i y_{t_p}>=y_i ytp>=yi,说明 i i i在一定凸包内部,必然形成下凸点,可以扔掉,结束。
- 否则,删除 t p t_p tp,加入节点 i i i
- 如果
x
t
p
x_{t_p}
xtp和
x
i
x_i
xi不相等
- 如果 t p − 1 t_{p-1} tp−1, i i i, t p t_p tp构成下凸关系,就扔掉 i i i,结束。
- 否则,加入节点 i i i
- 现在已经把节点 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,一直重复直到没有下凸或到边界为止。
- 对于左边的位置 l p = p − 1 lp=p-1 lp=p−1,同样进行上述操作,每次删去凸点后给 l p − 1 lp-1 lp−1。
查找斜率时,不建议再次二分,而是直接用平衡树自带的二分性质。
ㅤ
为此,我们最好把每个点右边线段的斜率也直接用平衡树维护。
ㅤ
插入过程中哪些斜率发生了变化?
ㅤ
维护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;
}