AcWing 301 任务安排2

不打扰是莪最后的温柔 提交于 2020-03-03 23:45:18

题目描述:

有 N 个任务排成一个序列在一台机器上等待执行,它们的顺序不得改变。

机器会把这 N 个任务分成若干批,每一批包含连续的若干个任务。

从时刻0开始,任务被分批加工,执行第 i 个任务所需的时间是 Ti。

另外,在每批任务开始前,机器需要 S 的启动时间,故执行一批任务所需的时间是启动时间 S 加上每个任务所需时间之和。

一个任务执行后,将在机器中稍作等待,直至该批任务全部执行完毕。

也就是说,同一批任务将在同一时刻完成。

每个任务的费用是它的完成时刻乘以一个费用系数 Ci。

请为机器规划一个分组方案,使得总费用最小。

输入格式

第一行包含整数 N。

第二行包含整数 S。

接下来N行每行有一对整数,分别为 Ti 和 Ci,表示第 i 个任务单独完成所需的时间 Ti 及其费用系数 Ci。

输出格式

输出一个整数,表示最小总费用。

数据范围

1≤N≤3∗10^5,
1≤Ti,Ci≤512,
0≤S≤512

输入样例:

5
1
1 3
3 2
4 3
2 3
1 4

输出样例:

153

分析:

AcWing 300 任务安排1中,我们使用了费用提前计算的技巧,实现了本题平方级别的算法,对于本题增加的数据范围,显然平方级别的算法不足以解决问题,我们在上一题中推导出了本题的状态转移方程f[i] = min(f[j] + (sc[i] - sc[j])*st[i] + (sc[n] - sc[j])*S),现在需要想办法在线性的时间内解决本题。

本题需要使用斜率优化DP,又被称为凸包(凸壳)优化。虽然说最后的结果不是很复杂,但是没有接触过凸包的情况下,想要理解本题,还是比较困难的。所以,先引入几个有关凸包的概念。先看下凸包的定义:

凸包(Convex Hull)是一个计算几何(图形学)中的概念。

在一个实数向量空间V中,对于给定集合X,所有包含X的凸集的交集S被称为X的凸包

X的凸包可以用X内所有点(X1,...Xn)的凸组合来构造.

在二维欧几里得空间中,凸包可想象为一条刚好包著所有点的橡皮圈。

用不严谨的话来讲,给定二维平面上的点集,凸包就是将最外层的点连接起来构成的凸多边形,它能包含点集中所有的点。

正如最后一句话所说,在一个点集中,外层的点构成的包含所有点集中的点的凸多边形就是凸包。

如上图所示,点集中外层的点构成的凸多边形就构成了能够包含所有点的凸包,其中连接相邻顶点构成的边越来越平缓,或者说斜率越来越小构成的一组点叫做上凸壳,而相邻的边,斜率越来越大的一组点叫做下凸壳。本题主要考虑的是下凸壳。

如图所示,开始只有顶点AB构成的凸包,然后加入第三点C1,显然BC1的斜率是高于AB的,因此AB,BC1构成了一个下凸壳;但是如果新加的点不是C1而是C2,BC2的斜率小于AB,那么AB和BC2就不能构成下凸壳了,因为不能作为点集的下边界,不能包含在AB下面却在AC2上面的点,因此,加入C2后,AC2将成为下凸壳新的边界了,因此,对于平面上的三点A(x1,y1),B(c2,y2),C(c3,y3),并且x1 < x2 < x3,y1 < y2 < y3。AB与BC能够作为凸包当且仅当AB的斜率要小于BC的斜率。

上面介绍的知识现在看还是看不出与本题的关联的,但是马上就会用到了,本题的求解步骤类似于凸包的Graham扫描法。对于状态转移方程f[i] = min(f[j] + (sc[i] - sc[j])*st[i] + (sc[n] - sc[j])*S),对能够使f[i]最小的j,有f[i] = f[j] + (sc[i] - sc[j])*st[i] + (sc[n] - sc[j])*S,由于在求f[i]时,sc[i],sc[j],S都是已知量,对式子做下简单变形,得到f[j] = (st[i] + S) * sc[j] + f[i] - sc[i] * st[i] - sc[n] * S,可以发现 - sc[i] * st[i] - sc[n] * S是常数,f[j]和sc[j]是变量,我们需要做的是,找到合适的j,使得f[i]最小,将f[j]看作y,sc[j]看作x,st[i] + S看作k,f[i] - sc[i] * st[i] - sc[n] * S看作b,式子就化作了y = kx + b这样简单的一次函数,f[i]最小时,截距也最小,所以问题就转化为了在所有(sc[j],f[j])构成的点集中,找到一条斜率为st[i] + S的直线,使得截距最小。

如图所示,我们用一条斜率为k = st[i] + S的直线不断往上移动,最先碰到点集中的点时,此时的截距最小,f[i]也最小。可以确定的是,最先碰到的一定是凸多边形边界的顶点,而不会是凸包内部包含的点,所以,凸包以外的点可以排除了。现在,需要确定的是,使得截距最小的到底是凸包上的哪一点,图中直线上移最先遇见的显然是B点,那在什么情况下最先遇见的不是AC而是B点呢,只需要AB的斜率小于k,BC的斜率大于k即可,也就说,我们需要维护一个队列,自队头到队尾斜率递增,我们从头遍历队列,最先遇见大于k的斜率的端点就是所求的j点。我们在遍历i的时候逐步的去求f[i],求f[i]的时候用到了0到i-1,也就说所有的j,每次求出了i也需要将i作为i + 1状态中j的候选者加入到点集中,也就说,上图中的点集分别是(sc[0],f[0]),(sc[1],f[1]),...,由于sc是前缀和,而且c数组都是整数,所以随着i的增加,新加入的(sc[j],f[j])一定是横纵坐标都超过之前的点的,我们需要不停的向点集中添加横纵坐标都增加的点,并且更新加入新点后点集的凸包,还记得开始我们提过的结论:对于平面上的三点A(x1,y1),B(c2,y2),C(c3,y3),并且x1 < x2 < x3,y1 < y2 < y3。AB与BC能够作为凸包当且仅当AB的斜率要小于BC的斜率。也就说,原本凸包上有AB构成的边,加入C后,要想BC也作为凸包的边界,只需要BC的斜率大于AB,否则,就要删除B点,因为AC肯定在B点的下方,B不能作为凸包的边界了。

整理下思路,我们遍历i的时候,相当于不断的往点集中加点,并且更新凸包,在求f[i]时,只需要找到凸包中第一个大于k的斜率即可,由于凸包的边的斜率自左向右是递增的,所以我们可以维护一个队列,队头的斜率最小,并且自队头向队尾斜率是递增的。我们还需要注意求f[i]时候的k = st[i] + S,随着i的增加k也在增加,所以队列中小于k的斜率肯定也小于后面的k,不可能成为最优解的,所以在遍历队列找第一个大于k的斜率时可以将小于k的斜率都删掉。另外,为了维护队列的单调性,插入新点(sc[i],f[i])时,如果该点与之前相邻点的斜率不是高于之前队尾的斜率的,就要将队尾的斜率出队,踢出凸包的点集,直至找到一个合适的位置再插入队列。

最后只剩下一步,就是将上面单调队列的思路实现为具体的代码。单调队列中存储的必然是点的编号i,其代表的斜率应该与其更接近队尾的相邻的点构成的斜率,所以初始情况下队头应该有个哨兵节点0,后面要考虑的就是何时出队头,何时出队尾了。

出队头一方面是为了自小到大找到第一个大于k的斜率,另一方面是为了删掉小于k的斜率,一条线至少有两个点构成,所以只有队列中有不少于两个点的情况下才能出队头,队头存储的是(sc[q[hh]],f[q[hh]]),与后一个点(sc[q[hh]+1],f[q[hh]+1])构成的斜率是(f[q[hh]+1] - f[q[hh]]) / (sc[q[hh]+1] - sc[q[hh]]),当该斜率小于st[i] + S时就要出队头考虑下一个斜率,为了避免除法实现时需要变形下改为乘法。

出完队头就找到了最优解j,开始更新f[i],任何就是将(sc[i],f[i])加入队列,同样是需要比较斜率大小,将该点放在合适的位置使得队列斜率递增,当(f[i] - f[q[tt]]) / (sc[i] - sc[q[tt]]) > (f[q[tt]] - f[q[tt]-1]) / (sc[q[tt]] - sc[q[tt]-1])时才能加入到队尾。

另外,由于计算过程中的乘法可能会爆int,所以本题的数组定义为long long类型。

#include <iostream>
#include <algorithm>
using namespace std;
const int N = 300005;
typedef long long ll;
ll f[N],t[N],c[N];
int q[N];
int main(){
    int n,s;
    scanf("%d%d",&n,&s);
    for(int i = 1;i <= n;i++){
        scanf("%d%d",&t[i],&c[i]);
        t[i] += t[i-1],c[i] += c[i-1];
    }
    int hh = 0,tt = 0;
    for(int i = 1;i <= n;i++){
        while(hh < tt && f[q[hh+1]]-f[q[hh]]<=(t[i]+s)*(c[q[hh+1]]-c[q[hh]])) hh++;
        f[i] = f[q[hh]] - (t[i] + s) * c[q[hh]] + c[i] * t[i] + s * c[n];
        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;
}

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!