#3023 任务安排

题面
N个任务排成一个序列在一台机器上等待完成(顺序不得改变),这N个任务被分成若干批,每批包含相邻的若干任务。从时刻0开始,这些任务被分批加工,第i个任务单独完成所需的时间是Ti。在每批任务开始前,机器需要启动时间S,而完成这批任务所需的时间是各个任务需要时间的总和(同一批任务将在同一时刻完成)。每个任务的费用是它的完成时刻乘以一个费用系数Fi。请确定一个分组方案,使得总费用最小。
例如:S=1;T={1,3,4,2,1};F={3,2,3,3,4}。如果分组方案是{1,2}、{3}、{4,5},则完成时间分别为{5,5,10,14,14},费用C={15,10,30,42,56},总费用就是153。

输入
第一行是N(1< =N< =5000)。
第二行是S(0< =S< =50)。
下面N行每行有一对数,分别为Ti和Fi,均为不大于100的正整数,表示第i个任务单独完成所需的时间是Ti及其费用系数Fi。

输出
一个数,最小的总费用。

样例输入
5
1
1 3
3 2
4 3
2 3
1 4

样例输出
153

提示
1<=N<=10000,1<=S<=50,1<=Ti,ci<=100

SOL
斜率优化的模板题之一。
最开始很容易想到一个二维的状态定义f[i][j]f[i][j]表示前ii个任务被分成jj批完成的最小费用,但是这样显然是O(N3)O(N^3)的。
于是想到去掉完成批数的那一维——因为你不需要知道当前做到了第几批,你只需要知道你再分出一批来对后继状态的影响。
于是有:
f[i]=min(f[i],f[j]+t[i](c[i]c[j])+s(c[n]c[j]))(0&lt;=j&lt;i)f[i]=min(f[i],f[j]+t[i]*(c[i]-c[j])+s*(c[n]-c[j])) (0&lt;=j&lt;i)
注意:t[i],c[i]t[i],c[i]均为前缀和,不是题目描述的那种。
你将[j+1,i][j+1,i]分为新的一批,会使后面的所有任务总共增加s(c[n]c[j])s*(c[n]-c[j])的费用。
于是算法被优化到了O(N2)O(N^2),可以过N&lt;=5000N&lt;=5000的点。
还可不可以再优化呢?看到min我们容易去想用一个单调队列去维护,但是直接维护这个min显然不可行。
这时候我们如果抛开min,将整个式子看成一个函数,于是你可以发现两个变量——f[j],c[j]f[j],c[j]。使前者作为因变量,后者为自变量,则可以得到如下函数:
f[j]=(s+t[i])c[j]+f[r]t[i]c[i]sc[n]f[j]=(s+t[i])*c[j]+f[r]-t[i]*c[i]-s*c[n]
这很像y=kx+by=kx+b,有斜率,有截距,而且截距最小的时候,f[i]f[i]最小。
于是考虑怎样使截距最小。
如果把f[j]f[j]构成的点集表示在函数图像上,应该是这样的:
[FROM WOJ]#3023 任务安排
看上去很乱,但是如果你把相邻的三个点j1,j2,j3j1,j2,j3单独拿出来看:
[FROM WOJ]#3023 任务安排
于是可以知道,当k(j1,j2)&lt;k(j2,j3)k(j1,j2)&lt;k(j2,j3)时,j2j2才是有效决策。
k(x,y)k(x,y)表示x,yx,y连线的斜率)
所以我们应该维护一个相邻两点连线斜率的单调递增队列,即一个下凸壳。
[FROM WOJ]#3023 任务安排
我们只需要保留斜率大于k、单调递增的部分即可。
这样做的效率是O(N)O(N)的。

代码:

#include<bits/stdc++.h>
#define N 10005
#define int long long
using namespace std;
inline int rd(){
	int data=0,w=1;static char ch=0;ch=getchar();
	while(!isdigit(ch)&&ch!='-')ch=getchar();
	if(ch=='-')w=-1,ch=getchar();
	while(isdigit(ch))data=(data<<1)+(data<<3)+ch-'0',ch=getchar();
	return data*w;
}
int n,s,f[N],t[N],c[N],q[N];
signed main(){
	n=rd();s=rd();
	for(int register i=1;i<=n;i++){
		int register ti=rd(),ci=rd();
		t[i]=t[i-1]+ti;c[i]=c[i-1]+ci;
	}
	memset(f,0x5f,sizeof(f));f[0]=0;
	int l=1,r=1;q[l]=0;
	for(int register i=1;i<=n;i++){
		int k=s+t[i];
		while(l<r&&(f[q[l+1]]-f[q[l]])<=k*(c[q[l+1]]-c[q[l]]))l++;
		//维护斜率大于k 
		f[i]=f[q[l]]-k*c[q[l]]+t[i]*c[i]+s*c[n];
		while(l<r&&(f[q[r]]-f[q[r-1]])*(c[i]-c[q[r]])>=(f[i]-f[q[r]])*(c[q[r]]-c[q[r-1]]))r--;
		//维护下凹性质 
		q[++r]=i;
	}
	printf("%lld",f[n]);
	return 0;
}

Update:
如果ti可以为负数呢?
s+t[i]不能单调递增,因此此时维护的不只是大于k的部分,需要维护整个下凸壳,因此需要二分一下找到那个左边斜率小于k且右边斜率大于k的点。
二分代码:

inline int search(int i,int k){
	if(l==r)return q[l];
	int L=l,R=r;
	while(L<R){
		int mid=(L+R)>>1;
		if(f[q[mid+1]]-f[q[mid]]<=k*(c[q[mid+1]]-c[q[mid]]))L=mid+1;
		else R=mid;
	}
	return q[L];
}

题面详见WOJ #3622

相关文章: