• AcWing 301. 任务安排2


    题目传送门

    一、分析过程

    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;
    }
    
    
  • 相关阅读:
    Flutter实战视频-移动电商-11.首页_屏幕适配方案讲解
    Flutter实战视频-移动电商-10.首页_FlutterSwiper轮播效果制作
    Flutter实战视频-移动电商-09.首页_项目结构建立和获取数据
    Flutter实战视频-移动电商-08.Dio基础_伪造请求头获取数据
    【RQNOJ】460 诺诺的队列
    NYOJ 483 Nightmare 【广搜】+【无标记】
    冒泡,简单选择,直接插入排序(Java版)
    基于MFC与第三方类CWebPage的百度地图API开发范例
    USACO 1.2 Palindromic Squares (进制转换,回文)
    与《新走遍美国》的邂逅
  • 原文地址:https://www.cnblogs.com/littlehb/p/15820974.html
Copyright © 2020-2023  润新知