zl程序教程

您现在的位置是:首页 >  其它

当前栏目

AcWing 301. 任务安排2

任务 AcWing 安排 301
2023-09-27 14:28:12 时间

题目传送门

一、分析过程

1、与上题区别

\(AcWing\) \(300\) 任务安排\(1\)中,我们使用了费用提前计算技巧,实现了本题平方级别的算法,对于本题增加的数据范围,\(n<=300000\),显然平方级别的算法不足以解决问题,需要想办法在线性\(O(N)\)的时间内解决本题。

2、优化的思路

对于状态转移方程

\[f[i] = min\{ f[j] + (sc[i] - sc[j])*st[i] + (sc[n] - sc[j])*S \} \]

其中\(0<=j<i\)
联系题意,\(j\)表示的其实是第\(i\)个批次任务的前一个批次任务的最后一个节点,在上一题中,通过遍历每一个可能的节点,然后找到合理的节点\(j\)

由于在求\(f[i]\)时,\(sc[i]\),\(sc[n]\),\(S\)都是已知量,对式子做下简单变形,(变化的一堆,不变的一堆)得到:

\[f[j] = (st[i] + S) * sc[j] + f[i] - sc[i] * st[i] - sc[n] * S \]

把变化的\(f[j]\),\(sc[j]\)放到了左侧,把一些在确定\(i\)时刻时的常量放到了右侧。

目标:找到合适的\(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\)这样简单的一次函数

因为\(- sc[i] * st[i] - sc[n] * S\)是固定值,所以\(f[i]\)最小时,截距\(b\)也就最小,问题就转化为在所有\((sc[j],f[j])\)构成的点集中,找到一条斜率为\(k=st[i] + S\)的直线,使得截距\(b\)最小。

这不就变成了直线方程\(y=kx+b\)与一个点集相交问题了吗?用我们学过的凸包知识来优化它了!如果我们动态维护好了凸包,那么就会大大减少计算量!

二、代码逻辑

1、凸包里的内容

凸包里保存的是一个斜率单调递增队列,它里面装的是二维坐标,也就是\(x=sc[j],y=f[j]\)

2、出队列头

我们遍历\(i\)的时候,相当于不断往点集中加点,并且更新凸包。新增加进来的元素当然也是二维坐标,即\(x=sc[j],y=f[j]\)

在求\(f[i]\)时,只需要找到凸包中第一个大于\(k\)的斜率即可,由于凸包的边的斜率自左向右是递增的,所以我们可以维护一个队列队头的斜率最小,并且自队头向队尾斜率是递增的

新二维坐标元素的加入,可能会破坏原来的凸包,因为虽然横坐标\(sc[j]\)是一直增加的,\(y=f[j]\)也是不断增加的,但是

\(C\)点可能是\(C_1\),也可能是\(C_2\),这两种情况下,\(x,y\)都是在增长,但效果完全不一样,凸包的边有可能变成\(ABC_1\),也可能删除\(B\)变成\(AC_2\)

总结:随着\(i\)的不断从左到右变化,每加入一次点,凸包都会产生相应的变化。下个直线方程再来找与凸包的交点时,也需要再次在凸包的队列中找出两个斜率之间的位置。

我们还注意到求\(f[i]\)时候的\(k = st[i] + S\),随着\(i\)的增加\(k\)也在增加,所以队列中小于\(k\)的斜率肯定也小于后面的斜率\(k\),不可能成为最优解的,所以在遍历队列找到第一个大于\(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\)时就要出队头考虑下一个斜率,为了避免除法实现时需要变形下改为乘法。

  //出队头 注意这里是 hh < tt ,表示最少队列中有两个点的信息!
  while (hh < tt && f[q[hh + 1]] - f[q[hh]] <= (st[i] + s) * (sc[q[hh + 1]] - sc[q[hh]])) hh++;

出队头一方面是为了自小到大找到第一个大于\(k\)的斜率,另一方面是为了删掉小于\(k\)的斜率

2、加入到队列

出完队头就找到了最优解\(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])\)时才能加入到队尾。

3、出队列尾

为了维护队列的单调性,插入新点\((sc[i],f[i])\)时,如果该点与之前相邻点的斜率不是高于之前队尾的斜率的,就要将队尾的斜率出队,踢出凸包的点集,直至找到一个合适的位置再插入队列。

4、哨兵

单调队列中存储的是点的编号\(i\),其代表的斜率应该与其更接近队尾的相邻的点构成的斜率,所以初始情况下队头应该有个哨兵节点\(0\)

  int hh = 0, tt = -1;
  q[++tt] = 0;//加入哨兵

另外,由于计算过程中的乘法可能会爆\(int\),所以本题的数组定义为\(long\) \(long\)类型,但本题的坑点是:即使使用了\(long\) \(long\)类型,因为公式里面有乘法的原因,依然可能会爆掉,所以还需要在乘法时采用\(\_\_int128\)

三、实现代码

#include <bits/stdc++.h>

using namespace std;
typedef long long LL;
const int N = 300010;
LL st[N];       //表示前i个任务完成所需的时间前缀和sum(Ti)
LL sc[N];       //表示前i个任务完成所需的费用前缀和sum(Ci)
LL f[N];        //总费用最小值
int q[N];       //单调队列

int main() {
    int n, s;
    cin >> n >> s;

    for (int i = 1; i <= n; i++) cin >> st[i] >> sc[i], st[i] += st[i - 1], sc[i] += sc[i - 1];
    int hh = 0, tt = -1;
    q[++tt] = 0;//加入哨兵
    for (int i = 1; i <= n; i++) {
        //出队头
        while (hh < tt && f[q[hh + 1]] - f[q[hh]] <= (st[i] + s) * (sc[q[hh + 1]] - sc[q[hh]])) hh++;

        //利用公式更新最优解
        f[i] = f[q[hh]] - (st[i] + s) * sc[q[hh]] + sc[i] * st[i] + s * sc[n];

        /**
        本题之前的数据由于标程溢出long long类型,所以部分数据有误,现已更正。
        大家在做本题时需要注意两个long long类型的数据相乘需要强制类型转化成__int128以避免溢出问题。
         */
        //出队尾
        while (hh < tt &&
               (__int128) ((f[q[tt]] - f[q[tt - 1]]) * (sc[i] - sc[q[tt]])) >=
               (__int128) ((f[i] - f[q[tt]]) * (sc[q[tt]] - sc[q[tt - 1]])))
            tt--;
        //加入
        q[++tt] = i;
    }
    //输出
    printf("%lld\n", f[n]);
    return 0;
}