【IOI2002】任务安排2(斜率优化dp)

设sumt[i]表示前i个任务所用时间的和,sumc[i]表示前i个任务1单位时间所需的费用和。

设f[i]表示前i个任务完成所需最短时间,这里直接给出状态转移方程:

f[i]=min(f[j]+sumt[i]*(sumc[i]-sumc[j])+S*(sumc[N]-sumc[j]))

将f[j]和sumc[j]当做变量分离出来,并且去掉min,可以得到:

f[j]=(S+sumt[i])*sumc[j]+f[i]-sumt[i]*sumc[i]-S*sumc[N] 。

可以发现,如果一般的一次函数为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),或者只有一个点。因此第二个限制就是确保右侧的斜率大于左侧的斜率。

如图例,此时就满足:\frac{f[3]-f[2]}{sumc[3]-sumc[2]}>\frac{f[2]-f[1]}{sumc[2]-sumc[1]},即右侧斜率大于左侧。此时因为(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;
}

 

猜你喜欢

转载自blog.csdn.net/WWWengine/article/details/82225283