Problem
小X要进行D天的修炼,初始时他有C点魂力。有N件魂导器,第i件会在第Di天以Pi点魂力的价格售出,他有它时则可以用它修炼,每天获得Gi点魂力,他可以以Ri点魂力的代价卖掉它。小X魂力不足Pi不能买。小X有买卖的日子里不能修炼。D天结束后,小X卖掉手头的魂导器。求最后至多能有多少魂力。
Input
Output
Hint
Solution
首先我一看题:魂力、魂导器……这不是斗罗大陆吗?!(什么时候魂导器还能用魂力交易了!!!)
那么这题一眼DP了。
30points:DP
容易想到以d为关键字,将魂导器从小到大排序,然后DP。
设f[i]表示到达第d[i]天且手中没有魂导器时的最大魂力。那么有边界:f[0]=C,d[0]=0,d[n+1]=D+1。那么极易推出朴素的转移:
时间复杂度: 。
Code
代码就不贴了,反正下面的60points做法也有。
60points:斜率优化
我们瞟到4、5、6这仨点有个特殊性质:对于任意i、j,若Di≥Dj,那么Gi≥Gj。而且基于这种题一般都会用斜率优化,那么我们不妨也推一推。
首先,沿袭30points做法的状态设法。我们假设j<k<i,且由f[j]转移到f[i]较f[k]转移到f[i]更优。从f[j]转移到f[i]的式子可化简为:
容易发现前面红色的部分只与j有关,不妨设其为v[j],则由f[j]转移到f[i]较f[k]转移到f[i]更优就是要满足:
移项可得:
哈哈,由于此时的j<k,d[j]就≤d[k],g[j]也自然而然≤g[k],可以确定其正负,可以从容约分:
右边即为j、k两点间直线的斜率,不妨设其为G(j,k)。那么当d[i]≤G(j,k)时,j比k优。
那么若有三点j1,k1,k2,且它们满足G(j1,k1)≥G(k1,k2),则说明当k1比k2优时,j1肯定也比k1优,我们可删去k1这个点。这样我们就需维护一个单调不减的斜率。
注意到d[i]是单调不减的,所以我们可以不必二分,直接取队首转移。当然,要注意弹栈,且计算斜率时g[j]可能=g[k],要注意特判。
最后,友情提醒:1. 计算完f[i]后,要判断f[i]足不足以支持它买下第i个魂导器,足以才能拿它转移其他的;2.由于条件是“若Di≥Dj,那么Gi≥Gj”,那么Di可能=Dj,所以 务必要拿G做排序的第二关键字。我就被这两点坑了好久。
时间复杂度:快排 ,DP 。
Code
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
#define Lf long double
#define fo(i,a,b) for(i=a;i<=b;i++)
#define fd(i,a,b) for(i=a;i>=b;i--)
const int N=1e5+10;
int i,j,k,num,q,n,sta[N],head,tail;
Lf c,d,C,f[N],v[N];
struct hdq
{
Lf d,p,r,g,s;
}a[N];
inline bool compare(hdq a,hdq b)
{
return a.d<b.d||a.d==b.d&&a.g<b.g;
}
inline Lf G(int j,int k)
{
return (v[k]-v[j])/(a[j].g-a[k].g);
}
inline bool cmp(int j1,int k1,int k2)
{
if(a[j1].g==a[k1].g)return v[k1]-v[j1]<0;
if(a[k1].g==a[k2].g)return v[k2]-v[k1]>=0;
return G(j1,k1)>=G(k1,k2);
}
void work()
{
memset(f,0,sizeof f);
f[0]=v[0]=c;
a[n+1].d=d+1;
if(num<4)
{
fo(i,1,n+1)
fo(j,0,i-1)
if(f[j]>=a[j].p)
f[i]=max(f[i],f[j]+a[j].s+a[j].g*a[i].d);
return;
}
sta[head=tail=1]=0;
fo(i,1,n+1)
{
while(head<tail)
{
j=sta[head];k=sta[head+1];
if(a[i].d*(a[j].g-a[k].g)<=v[k]-v[j])
head++;
else break;
}
j=sta[head];
f[i]=v[j]+a[j].g*a[i].d;
if(f[i]>=a[i].p)
{
v[i]=f[i]+a[i].s;
while(head<tail&&cmp(sta[tail-1],sta[tail],i))tail--;
sta[++tail]=i;
}
}
}
int main()
{
freopen("practice.in","r",stdin);
freopen("practice.out","w",stdout);
for(scanf("%d%d",&num,&q);q;q--)
{
scanf("%d%Lf%Lf",&n,&c,&d);
memset(a,0,sizeof a);
fo(i,1,n)
{
scanf("%Lf%Lf%Lf%Lf",&a[i].d,&a[i].p,&a[i].r,&a[i].g);
C=a[i].r-a[i].p;
a[i].s=C-a[i].g*(1+a[i].d);
}
sort(a+1,a+n+1,compare);
work();
printf("%.0Lf\n",f[n+1]);
}
}
100points:排序关键字
上述做法都是拿d作关键字,其实我们也可以拿g作关键字,因为我们肯定是从g小的魂导器转移到g大的魂导器。转移和斜率优化与60points方法一致,不同之处就在于将g的不单调转变成了d的不单调。那么,转移时就二分一下即可。
时间复杂度:
。
Code
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
#define Lf long double
#define fo(i,a,b) for(i=a;i<=b;i++)
#define fd(i,a,b) for(i=a;i>=b;i--)
const int N=1e5+10;
int i,j,k,num,q,n,sta[N],top;
Lf c,d,C,f[N],v[N];
struct hdq
{
Lf d,p,r,g,s;
}a[N];
inline bool compare(hdq a,hdq b)
{
return a.g<b.g;
}
inline bool better(int j,int k)
{
return a[i].d*(a[j].g-a[k].g)>v[k]-v[j];
}
inline Lf G(int j,int k)
{
return (v[k]-v[j])/(a[j].g-a[k].g);
}
inline bool cmp(int j1,int k1,int k2)
{
if(a[j1].g==a[k1].g)return v[k1]-v[j1]<0;
if(a[k1].g==a[k2].g)return v[k2]-v[k1]>=0;
return G(j1,k1)>=G(k1,k2);
}
int bs(int l,int r)
{
int mid;
while(l<r)
{
mid=(l+r+1)>>1;
if(better(sta[mid-1],sta[mid]))
r=mid-1;
else l=mid;
}
return sta[l];
}
void work()
{
memset(f,0,sizeof f);
f[0]=v[0]=c;
a[n+1].d=d+1;
sta[top=1]=0;
fo(i,1,n+1)
{
j=bs(1,top);
f[i]=v[j]+a[j].g*a[i].d;
if(f[i]>=a[i].p)
{
v[i]=f[i]+a[i].s;
while(top>1&&cmp(sta[top-1],sta[top],i))top--;
sta[++top]=i;
}
}
}
int main()
{
freopen("practice.in","r",stdin);
freopen("practice.out","w",stdout);
for(scanf("%d%d",&num,&q);q;q--)
{
scanf("%d%Lf%Lf",&n,&c,&d);
memset(a,0,sizeof a);
fo(i,1,n)
{
scanf("%Lf%Lf%Lf%Lf",&a[i].d,&a[i].p,&a[i].r,&a[i].g);
C=a[i].r-a[i].p;
a[i].s=C-a[i].g*(1+a[i].d);
}
sort(a+1,a+n+1,compare);
work();
printf("%.0Lf\n",f[n+1]);
}
}