题目描述
有 N N N 个任务排成一个序列在一台机器上等待执行,它们的顺序不得改变。
机器会把这 N N N 个任务分成若干批,每一批包含连续的若干个任务。
从时刻 0 0 0开始,任务被分批加工,执行第 i i i 个任务所需的时间是 T i T_i Ti。
另外,在每批任务开始前,机器需要 S 的启动时间,故执行一批任务所需的时间是启动时间 S S S 加上每个任务所需时间之和。
一个任务执行后,将在机器中稍作等待,直至该批任务全部执行完毕。
也就是说,同一批任务将在同一时刻完成。
每个任务的费用是它的完成时刻乘以一个费用系数 C i C_i Ci。
请为机器规划一个分组方案,使得总费用最小。
输入格式
第一行包含整数 N N N。
第二行包含整数 S S S。
接下来N行每行有一对整数,分别为 T i T_i Ti 和 C i C_i Ci,表示第 i i i 个任务单独完成所需的时间 T i T_i Ti 及其费用系数 C i C_i Ci。
输出格式
输出一个整数,表示最小总费用。
数据范围
1
≤
N
≤
3
×
1
0
5
1≤N≤3\times10^5
1≤N≤3×105 ,
1
≤
T
i
,
C
i
≤
512
1≤T_i,C_i≤512
1≤Ti,Ci≤512,
0
≤
S
≤
512
0≤S≤512
0≤S≤512
输入样例
5
1
1 3
3 2
4 3
2 3
1 4
输出样例
153
算法思想(斜率优化)
在任务安排1中,我们使用了时间复杂度为
O
(
n
2
)
O(n^2)
O(n2)的算法解决了该问题,在本题中会TLE。我们来分析一下状态转移方程:
f
[
i
]
=
m
i
n
{
f
[
j
]
+
S
u
m
T
i
×
(
S
u
m
C
i
−
S
u
m
C
j
)
+
S
×
(
S
u
m
C
n
−
S
u
m
C
j
)
}
,
0
≤
j
<
i
f[i]= min\{f[j] + Sum_{T_i}\times(Sum_{C_i}-Sum_{C_j})+S\times (Sum_{C_n}-Sum_{C_j})\}, 0\le j \lt i
f[i]=min{f[j]+SumTi×(SumCi−SumCj)+S×(SumCn−SumCj)},0≤j<i
对于
f
[
i
]
f[i]
f[i]来说,
S
u
m
T
i
、
S
u
m
C
i
、
S
、
S
u
m
C
n
Sum_{T_i}、Sum_{C_i}、S、Sum_{C_n}
SumTi、SumCi、S、SumCn都是确定的值,只有
f
[
j
]
、
S
u
m
C
j
f[j]、Sum_{C_j}
f[j]、SumCj是需要进行枚举的,不妨设
y
=
f
[
j
]
,
x
=
S
u
m
C
j
y=f[j], x=Sum_{C_j}
y=f[j],x=SumCj,将上式重新整理可得:
y
=
(
S
u
m
T
i
+
S
)
×
x
+
f
[
i
]
−
S
u
m
T
i
×
S
u
m
C
i
−
S
×
S
u
m
C
n
y = (Sum_{T_i}+S)\times x + f[i] - Sum_{T_i} \times Sum_{C_i} -S\times Sum_{C_n}
y=(SumTi+S)×x+f[i]−SumTi×SumCi−S×SumCn
分析上式发现,对于不同的 j ( 0 ≤ j < i ) j(0\le j \lt i) j(0≤j<i),一共有 i i i对不同的 ( x , y ) (x,y) (x,y),如果在平面直角坐标系上表示出来,就是一系列斜率 k = ( S u m T i + S ) k=(Sum_{T_i}+S) k=(SumTi+S),截距 b = f [ i ] − S u m T i × S u m C i − S × S u m C n b=f[i] - Sum_{T_i} \times Sum_{C_i} -S\times Sum_{C_n} b=f[i]−SumTi×SumCi−S×SumCn的直线,形式如 y = k × x + b y=k\times x+b y=k×x+b。
我们目标是求最小的 f [ i ] f[i] f[i]。
斜率优化一:通过斜率确定最优解
分析题目要求可以发现,斜率k和y的值都大于0,因此当斜率
k
k
k相同时,截距
b
b
b越小
f
[
i
]
f[i]
f[i]越小。让截距更小,意味着直线更靠“下”,如下图所示,相比于
A
2
A_2
A2、经过
A
1
A_1
A1的蓝线截距更小。
那么对于上图来说,直线经过
B
B
B点时,其截距最小,此时
f
[
i
]
f[i]
f[i]可以取最小值。
这就是斜率优化的第一步:对于斜率相同的直线(即
i
i
i相同),为了让
f
[
i
]
f[i]
f[i]最小,就需要截距最小。可以想象成将直线从最下方向上移动,碰到的第一个点就是截距最小的点。
斜率优化二:去掉冗余点
进一步分析,对于所有可能的斜率
k
k
k(不同的
i
i
i对应的斜率不同)所确定的直线来说,某些点永远不可能被最小值所在的直线经过,如下图中红色的点:
分析规律发现,如果我们任选两个点确定一条直线,那么这个直线“上方”的点、截距会更大,不可能用来计算最小值。这个性质跟凸包的定义是一致的:
在一个实数向量空间 V V V中,对于给定集合 X X X,所有包含 X X X的凸集的交集 S S S被称为 X X X的凸包。 X X X的凸包可以用 X X X内所有点 ( X 1 , . . . X n ) (X_1,...X_n) (X1,...Xn)的凸组合来构造。
简单来说, 给你一个点集Q,你可以把Q中的每个点想象成一块木板上的铁钉,而点集Q的凸包就是包围了所有铁钉的一条拉紧了橡皮绳所构成的形状。
所以,斜率优化又称为凸包优化。根据上述性质,可以将红色的点全部删掉,那么剩下的蓝色点就构成了凸包的下边界,如图所示:
有了这个性质,我们在计算时,只需维护凸包下边界上的点即可,最小值
f
[
i
]
f[i]
f[i]一定在凸包下边界上,这就是斜率优化的核心思想。
斜率优化三:在凸包上找到截距最小的点
那么如何利用这个性质,求出给定直线(斜率为
k
k
k)在凸包边界上的点,从而得到该点对应的最小值
f
[
i
]
f[i]
f[i]呢?如下图所示:
从图中可以看出,斜率为
k
k
k的直线(红线),经过凸包边界上的
B
B
B点时,可以求得最小的
f
[
i
]
f[i]
f[i]。此时不妨设直线AB的斜率为
k
1
k1
k1、直线BC的斜率为
k
2
k2
k2,并且
k
1
<
k
<
k
2
k1<k<k2
k1<k<k2,不难发现当红线经过第一个斜率大于
k
k
k的点时,此时B点就是最小值所在点。
在本题中斜率 k ( S u m T i + S k (Sum_{T_i}+S k(SumTi+S)是单调递增的,向后计算时新加入点的 x ( S u m C j ) x(Sum_{C_j}) x(SumCj)坐标也是单调递增的,那么可以使用队列存储所有计算过的点:
- 在查询时,可以将队头小于等于当前斜率的点全部出队,即 f h h + 1 − f h h S u m C h h + 1 − S u m C h h ≤ S u m T i + S \frac{f_{hh+1}-f_{hh}}{Sum_{C_{hh+1}}-Sum_{C_{hh}}}\le Sum_{T_i}+S SumChh+1−SumChhfhh+1−fhh≤SumTi+S。因为斜率单调递增,后面只会越来越大。
- 在插入时,可以将队尾大于等于当前斜率的点(不在凸包上的点)出队,即 f t t − f t t − 1 S u m C t t − S u m C t t − 1 ≥ f i − f t t S u m C i − S u m C t t \frac{f_{tt}-f_{tt-1}}{Sum_{C_{tt}}-Sum_{C_{tt-1}}}\ge \frac{f_i-f_{tt}}{Sum_{C_i}-Sum_{C_{tt}}} SumCtt−SumCtt−1ftt−ftt−1≥SumCi−SumCttfi−ftt。因为不在凸包上,意味着不可能有最小值。
时间复杂度
对于每个点,只入队出队一次,所以时间复杂度为 O ( n ) O(n) O(n)。
代码实现
#include <iostream>
#include <cstdio>
using namespace std;
typedef long long LL;
const int N = 300010;
int q[N];
LL t[N], c[N], f[N];
int main()
{
int n, s;
scanf("%d%d", &n, &s);
for(int i = 1; i <= n; i ++)
{
scanf("%lld%lld", &t[i], &c[i]);
//处理前缀和
t[i] += t[i - 1], c[i] += c[i - 1];
}
int hh = 0, tt = 0;
q[tt] = 0; //将时刻0所对应的点加入队列
for(int i = 1; i <= n; i ++)
{
//队列中至少保留两个点,并且将队头小于等于当前斜率的点出队
while(hh < tt && (f[q[hh + 1]] - f[q[hh]]) <= (c[q[hh + 1]] - c[q[hh]]) * (t[i] + s)) hh ++;
int j = q[hh]; //队头即第一个大于当前斜率的点
//计算f[i]
f[i] = f[j] + t[i] * (c[i] - c[j]) + s * (c[n] - c[j]);
//队列中至少保留两个点,并且将队尾不在凸包上的点出队
while(hh < tt && (f[q[tt]] - f[q[tt - 1]]) * (c[i] - c[q[tt]]) >= (f[i] - f[q[tt]]) * (c[q[tt]] - c[q[tt - 1]])) tt --;
//将当前点加入队列
q[++ tt] = i;
}
printf("%lld\n", f[n]);
return 0;
}