#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]
f[i][j]表示前
i
i
i个任务被分成
j
j
j批完成的最小费用,但是这样显然是
O
(
N
3
)
O(N^3)
O(N3)的。
于是想到去掉完成批数的那一维——因为你不需要知道当前做到了第几批,你只需要知道你再分出一批来对后继状态的影响。
于是有:
f
[
i
]
=
m
i
n
(
f
[
i
]
,
f
[
j
]
+
t
[
i
]
∗
(
c
[
i
]
−
c
[
j
]
)
+
s
∗
(
c
[
n
]
−
c
[
j
]
)
)
(
0
<
=
j
<
i
)
f[i]=min(f[i],f[j]+t[i]*(c[i]-c[j])+s*(c[n]-c[j])) (0<=j<i)
f[i]=min(f[i],f[j]+t[i]∗(c[i]−c[j])+s∗(c[n]−c[j]))(0<=j<i)
注意:
t
[
i
]
,
c
[
i
]
t[i],c[i]
t[i],c[i]均为前缀和,不是题目描述的那种。
你将
[
j
+
1
,
i
]
[j+1,i]
[j+1,i]分为新的一批,会使后面的所有任务总共增加
s
∗
(
c
[
n
]
−
c
[
j
]
)
s*(c[n]-c[j])
s∗(c[n]−c[j])的费用。
于是算法被优化到了
O
(
N
2
)
O(N^2)
O(N2),可以过
N
<
=
5000
N<=5000
N<=5000的点。
还可不可以再优化呢?看到min我们容易去想用一个单调队列去维护,但是直接维护这个min显然不可行。
这时候我们如果抛开min,将整个式子看成一个函数,于是你可以发现两个变量——
f
[
j
]
,
c
[
j
]
f[j],c[j]
f[j],c[j]。使前者作为因变量,后者为自变量,则可以得到如下函数:
f
[
j
]
=
(
s
+
t
[
i
]
)
∗
c
[
j
]
+
f
[
i
]
−
t
[
i
]
∗
c
[
i
]
−
s
∗
c
[
n
]
f[j]=(s+t[i])*c[j]+f[i]-t[i]*c[i]-s*c[n]
f[j]=(s+t[i])∗c[j]+f[i]−t[i]∗c[i]−s∗c[n]
这很像
y
=
k
x
+
b
y=kx+b
y=kx+b,有斜率,有截距,而且截距最小的时候,
f
[
i
]
f[i]
f[i]最小。
于是考虑怎样使截距最小。
如果把
f
[
j
]
f[j]
f[j]构成的点集表示在函数图像上,应该是这样的:
看上去很乱,但是如果你把相邻的三个点
j
1
,
j
2
,
j
3
j1,j2,j3
j1,j2,j3单独拿出来看:
于是可以知道,当
k
(
j
1
,
j
2
)
<
k
(
j
2
,
j
3
)
k(j1,j2)<k(j2,j3)
k(j1,j2)<k(j2,j3)时,
j
2
j2
j2才是有效决策。
(
k
(
x
,
y
)
k(x,y)
k(x,y)表示
x
,
y
x,y
x,y连线的斜率)
所以我们应该维护一个相邻两点连线斜率的单调递增队列,即一个下凸壳。
事实上就是找一个最接近那条直线的一个点。
我们只需要保留斜率大于k、单调递增的部分即可。
用一个单调队列维护,则这样做的效率是
O
(
N
)
O(N)
O(N)的。
如果这样让你有些迷惑,不妨尝试用代数方法解释
当
k
>
j
k>j
k>j,且
k
k
k是一个相对更优的决策时,
f
[
k
]
+
t
[
i
]
∗
(
c
[
i
]
−
c
[
k
]
)
+
s
∗
(
c
[
n
]
−
c
[
k
]
)
<
=
f
[
j
]
+
t
[
i
]
∗
(
c
[
i
]
−
c
[
j
]
)
+
s
∗
(
c
[
n
]
−
c
[
j
]
)
f[k]+t[i]*(c[i]-c[k])+s*(c[n]-c[k])<=f[j]+t[i]*(c[i]-c[j])+s*(c[n]-c[j])
f[k]+t[i]∗(c[i]−c[k])+s∗(c[n]−c[k])<=f[j]+t[i]∗(c[i]−c[j])+s∗(c[n]−c[j])
那么化简可以得到:
s
+
t
[
i
]
>
=
f
[
k
]
−
f
[
j
]
/
(
c
[
k
]
−
c
[
j
]
)
s+t[i]>=f[k]-f[j]/(c[k]-c[j])
s+t[i]>=f[k]−f[j]/(c[k]−c[j])
所以当某一决策的斜率为大于
s
+
t
[
i
]
s+t[i]
s+t[i]的最小值,后面的决策都不能比它更优。
代码:
#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