其实解决这个问题的突破口在提示:
设f[i]表示第i天全部将A,B券换成人民币的最多数目,p[i].a表示A券在第i天最多拥有的个数,p[i].b表示B券在第i天最多拥有的个数,则:
那么,对于第i天最后一次购买,它在第j天(i<j)的折合人民币就是:
于是:
因此,我们得到一个动态规划的算法,框架如下:
p[1].b=S / (A[1] * Rate[1] + B [1])
Ans=S
For i = 2 to n ← 枚举在哪一天卖出券
For j = 1 to i-1 ← 枚举在哪一天买入券
x=p[ j ].a * A[i ] + p[j ].b * B [i ]
Ans=max{Ans,x}
End For
p [i ].b = Ans / (A[i ] * Rate[i ] + B[i ])
End For
Print(Ans)
我们可以把最后一次购入金劵的时间看作一种决策。现在考虑两种不同的决策在此时哪种能够获得最大的折合人民币。假设第i天A劵的价值为A[i],B劵的价值为B[i],两个不同的决策是:最后一次在第x天和第y天买入金劵。那么,决策x不如决策y优等价于:
这样我们就可以用平衡树以p[j ].a为关键字来维护一个凸线,平衡树维护一个点集(p[j ].a, p[j ].b),p[j ].a是单调递增的,相邻两个点的斜率是单调递减的.每次在平衡树中二分查找与-A[i ] / B[i ]最接近的两点之间的斜率
这样其实就转化成了斜率优化的动态规划
可是用平衡树维护,编程复杂度高了
事实上,我们可以利用分治的思想来提出一个编程复杂度比较低的方法:
首先我们按斜率-A[i]/B[i]从小到大排序,对于每一个i,它的决策j 的范围为1~i-1;我们定义一个Solve过程:Solve(l, r )
设mid=(l+r)/2,类似归并排序,我们用[l,r]的前半部[l,mid],来更新[l,r]区间的后半部[mid+1,r]的答案值,即在第i天我能得到的最大人民币数值,其中i∈[mid+1,r]
对于当前处理的区间[l,r],我们可以先Solve(l, mid ),这样保证在区间[l,mid]内的所有答案值均已被更新
然后对该区间的前半部建立一个凸壳(凸线),对于参与建立凸壳的点,需保证其水平序有序;用这个凸壳(凸线)去更新这个区间的后半部,其中后半部的所有元素,其-A[i]/B[i],即斜率,均有序,而凸壳的斜率也是单调的,这样正好可以用来解决本区间后半部的答案更新问题
而前半部分的排序只需在每次某区间全部处理完毕后,将该区间的所有元素<即将前半部分和后半部分合并>按水平序排序即可,后半部的元素在整个solve过程之前已整体按斜率排过一遍序
注意到,对于一个区间的前半部和后半部的作用是不同的,前半部的元素的答案值均已更新过答案值,为后半部的更新提供凸壳,后半部的元素的答案值更新由前半部构造的凸壳来更新答案,两部分的顺序互不影响。这样,我们只需要保证前半部的元素按水平序排序,后半部分按斜率排序,这样就相当于用一系列连续的直线去切一些连续点组成的凸壳。
给出一个算法框架:
Procedure Solve(l, r)
If l = r Then
直接更新答案值
Exit
Solve(l, mid )
→对[l, mid ]这一段扫描一遍计算出凸线
→扫描一遍更新[mid +1 , r]的最优决策
→ Solve(mid+1, r)
→将两部分合并,按水平序排序
End Procedure
时间复杂度分析:递归主过程:构造凸壳以及更新答案: O(n),那么递归方程为:T(n)=2T(n/2)+O(n),即时间复杂度为O(nlogn)
从这个例题中,我们可以看出,其solve过程的实质为:
→Solve(L,middle)
→处理[L,middle]中元素对[middle+1,R]中f[x]取值的影响
→ Solve(middle+1,R)
而在该问题中,处理影响部分相当于是更新答案值部分;使用了维护凸壳,斜率DP等方法来在尽可能优的时间内更新答案
附代码:
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<algorithm>
#include<iostream>
#include<cmath>
using namespace std;
#define MAXN 100001
#define EPS 1e-7
#define IMAX 21474836.0
struct DAY{double a,b,rate,k;int num;}a[MAXN],use[MAXN];
struct MONEY{double a,b;}p[MAXN],tmp[MAXN];
int N,stack[MAXN];
double S,f[MAXN];
bool cmp1(DAY X,DAY Y){return X.k<Y.k;}
double getk(int num1,int num2)
{
if(!num1) return -IMAX;
if(!num2) return IMAX;
if(fabs(p[num1].a-p[num2].a)<=EPS)
return IMAX;
return (p[num1].b-p[num2].b)/(p[num1].a-p[num2].a);
}
void mergesort(int left,int right)
{
int middle=(left+right)/2;
int L1=left,L2=middle+1;
for(int i=left;i<=right;i++)
{
if(((p[L1].a<p[L2].a || (fabs(p[L1].a-p[L2].a)<=EPS && p[L1].b<p[L2].b)) || L2>right) && L1<=middle)
tmp[i]=p[L1++];
else tmp[i]=p[L2++];
}
for(int i=left;i<=right;i++)
p[i]=tmp[i];
}
void solve(int left,int right)
{
if(left==right)
{
if(left==1) f[left]=f[left]<S?S:f[left];
else f[left]=f[left]<f[left-1]?f[left-1]:f[left];
p[left].b=f[left]/(a[left].a*a[left].rate+a[left].b);
p[left].a=p[left].b*a[left].rate;
return;
}
int middle=(left+right)/2;
int num1=left,num2=middle+1;
for(int i=left;i<=right;i++)
{
if(a[i].num<=middle)
use[num1++]=a[i];
else use[num2++]=a[i];
}
for(int i=left;i<=right;i++)
a[i]=use[i];
solve(left,middle);
int top=0,now=1;
for(int i=left;i<=middle;i++)
{
while(top-1>=1 && getk(i,stack[top])>getk(stack[top],stack[top-1]))
top--;
stack[++top]=i;
}
for(int i=right;i>=middle+1;i--)
{
while(now+1<=top && (a[i].k<getk(stack[now],stack[now+1])))
now++;
f[a[i].num]=max(f[a[i].num],a[i].a*p[stack[now]].a+a[i].b*p[stack[now]].b);
}
solve(middle+1,right);
mergesort(left,right);
}
int main()
{
//freopen("cash.in","r",stdin);
//freopen("cash.out","w",stdout);
scanf("%d%lf",&N,&S);
for(int i=1;i<=N;i++)
{
scanf("%lf%lf%lf",&a[i].a,&a[i].b,&a[i].rate);
a[i].k=-1*a[i].a/a[i].b;
a[i].num=i;
}
sort(a+1,a+1+N,cmp1);
solve(1,N);
printf("%.3lf\n",f[N]);
return 0;
}