斜率优化DP(一):任务安排2

题目描述

有 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​的蓝线截距更小。
斜率优化DP(一):任务安排2
那么对于上图来说,直线经过 B B B点时,其截距最小,此时 f [ i ] f[i] f[i]可以取最小值。
斜率优化DP(一):任务安排2
这就是斜率优化的第一步:对于斜率相同的直线(即 i i i相同),为了让 f [ i ] f[i] f[i]最小,就需要截距最小。可以想象成将直线从最下方向上移动,碰到的第一个点就是截距最小的点。

斜率优化二:去掉冗余点

进一步分析,对于所有可能的斜率 k k k(不同的 i i i对应的斜率不同)所确定的直线来说,某些点永远不可能被最小值所在的直线经过,如下图中红色的点:
斜率优化DP(一):任务安排2
分析规律发现,如果我们任选两个点确定一条直线,那么这个直线“上方”的点、截距会更大,不可能用来计算最小值。这个性质跟凸包的定义是一致的:

在一个实数向量空间 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的凸包就是包围了所有铁钉的一条拉紧了橡皮绳所构成的形状。

所以,斜率优化又称为凸包优化。根据上述性质,可以将红色的点全部删掉,那么剩下的蓝色点就构成了凸包的下边界,如图所示:
斜率优化DP(一):任务安排2
有了这个性质,我们在计算时,只需维护凸包下边界上的点即可,最小值 f [ i ] f[i] f[i]一定在凸包下边界上,这就是斜率优化的核心思想。

斜率优化三:在凸包上找到截距最小的点

那么如何利用这个性质,求出给定直线(斜率为 k k k)在凸包边界上的点,从而得到该点对应的最小值 f [ i ] f[i] f[i]呢?如下图所示:
斜率优化DP(一):任务安排2
从图中可以看出,斜率为 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​​−SumChh​​fhh+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−1​​ftt​−ftt−1​​≥SumCi​​−SumCtt​​fi​−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;
}


上一篇:LODOP获取打印机状态码和状态码含义测试


下一篇:Arthas 远程调试