设sumt[i]表示前i个任务所用时间的和,sumc[i]表示前i个任务1单位时间所需的费用和。
设f[i]表示前i个任务完成所需最短时间,这里直接给出状态转移方程:
。
将f[j]和sumc[j]当做变量分离出来,并且去掉min,可以得到:
。
可以发现,如果一般的一次函数为y=kx+b,那么此处的“y”就是f[j],“x”就是sumc[j],“k”就是S+sumt[i],“b”则是后面一大堆。
接下来有一些比较简单的数学东西,基本就是一次函数。
观察这个“k”,求解的是f[i],所以枚举到i,S+sumt[i]是确定的,相当于这条直线的斜率确定了。
再看后面的f[i]-sumt[i]*sumc[i]-S*sumc[N],这也是确定的,并且f[i]没有带上负号。也就是说,这条直线斜率不变时,图像越往下移f[i]越小。现在就需要确定可以下移到哪里。
我们把f[j]做y,sumc[j]做x的点描出来,再把这条直线画上去,发现可能是这样的:
如图,此时的直线与最下面的点2相交,f[i]取得最小值。
结合图象思考,f[i]每次取得最小值都是直线从下往上平移第一次碰到的点。所以我们要维护一个“下凸壳”,用单调队列实现。
一,由于sumt[i]在递增,斜率k也在变大,直线越来越陡,因此那些过于平坦的线段再也不会取到(如线段(1,2)),所以第一个限制就是队首的两个点之间斜率至少要大于S+sumt[i]。
二,一个点成为下凸点可能如图所示,斜率k(1,2)小于斜率k(2,3),或者只有一个点。因此第二个限制就是确保右侧的斜率大于左侧的斜率。
如图例,此时就满足:,即右侧斜率大于左侧。此时因为(1,2)的斜率小于直线的斜率了,1就该出队了。
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
typedef long long LL;
const int MAXN=10005;
int N,S,q[MAXN];
LL f[MAXN],sumt[MAXN],sumc[MAXN];
char c;
void scan(int &x)
{
for(c=getchar();c<'0'||c>'9';c=getchar());
for(x=0;c>='0'&&c<='9';c=getchar()) x=x*10+c-'0';
}
int main()
{
int i,t,c,L,R;
scan(N);scan(S);
for(i=1;i<=N;i++)
{
scan(t);scan(c);
sumt[i]=sumt[i-1]+(LL)t;
sumc[i]=sumc[i-1]+(LL)c;
}
memset(f,0x3f,sizeof(f));
f[0]=0;
L=1; R=1; //此处至少要有两个元素才行,故条件是L<R
for(i=1;i<=N;i++)
{
while(L<R && f[q[L+1]]-f[q[L]] <= (sumt[i]+S) * (sumc[q[L+1]]-sumc[q[L]]))
L++; //队首两个斜率太小了,去掉
f[i] = f[q[L]] - (S+sumt[i])*sumc[q[L]] + sumt[i]*sumc[i] + S*sumc[N];
while(L<R && (f[i]-f[q[R]]) * (sumc[q[R]]-sumc[q[R-1]]) <= (f[q[R]]-f[q[R-1]]) * (sumc[i]-sumc[q[R]]) )
R--; //队尾的斜率不比左侧大
q[++R]=i;
}
cout<<f[N];
return 0;
}