AcWing
  • 首页
  • 课程
  • 题库
  • 更多
    • 竞赛
    • 题解
    • 分享
    • 问答
    • 应用
    • 校园
  • 关闭
    历史记录
    清除记录
    猜你想搜
    AcWing热点
  • App
  • 登录/注册

AcWing 301. 任务安排2【斜率优化DP模板】    原题链接    困难

作者: 作者的头像   一只野生彩色铅笔 ,  2021-02-12 00:32:50 ,  所有人可见 ,  阅读 4583


80


24

最近在补全提高课所有题目的题解,宣传一下汇总的地方提高课题解补全计划

题目描述

有 $n$ 个 任务 排成一个 序列,顺序不得改变,其中第 $i$ 个 任务 的 耗时 为 $t_i$, 费用系数 为 $c_i$

现需要把该 $n$ 个 任务 分成 若干批 进行加工处理

每批次的 段头,需要 额外消耗 $S$ 的时间启动机器。每一个任务的 完成时间 是所在 批次 的 结束时间。

完成一个任务的 费用 为:从 $0$ 时刻 到该任务 所在批次结束 的时间 $t$ 乘以 该任务 费用系数 $c$

分析

关于本题朴素版的DP分析,可以看该篇博客:AcWing300. 任务安排1【线性DP+费用提前计算思想】

贴出朴素版的 状态转移方程:

$$ f_i = \min\bigg(f_j + S \times (sc_n - sc_j) + st_i \times (sc_i - sc_j)\bigg) $$

提出式子中 含有单独 $i$ 的常量:$f_i = st_i\times sc_i + S \times sc_n + \min\bigg(f_j - S \times sc_j - st_i \times sc_j\bigg)$

考察 $min$ 函数内部的 多项式:$f_j - sc_j \times (S + st_i)$

该式子中,有 $i \times j$ 的项,因此不能直接用上一章节中提到的 滑动窗口 来维护一个前缀的 最值

但是 分析的思想 可以继承,我们知道 含 $i$ 的项是一个 常量,故该 多项式 就能够 抽象 成如下形式:

$$ f_j - sc_j \times (S + st_i) = 变量1 - 变量2 \times (常量S + 常量i) $$

注意,这里的 变量1 和 变量2 并不是两个 独立变量 (该 函数 非 多元函数)

变量1 $f_j$ 是与 $j$ 有关的 变量,变量2 $sc_j$ 也是与 $j$ 有关的 变量

因此,不妨令 $\begin{cases} f_j = y(j) \\\\ sc_j = x(j) \\\\ k = S+st_i \end{cases}$,则该函数可以化为:$y(j) - k \cdot x(j)$

为了式子 方便观察,接下来我会把 $y(j)$ 写成 $y$,$x(j)$ 写成 $x$,但是请读者心中明白,这两个 变量,都是关于 $j$ 的 变量(学过 高数 的,直接类比 参数方程 即可)

求 $y - kx ~ (0 \le j \lt i)$ 函数的极值问题,可以直观想到 直线的斜截式方程:$y = kx + b$

对 直线的斜截式方程 进行变形:$b = y - kx$

要求 $y - kx ~ (0 \le j \lt i)$ 函数的极值,就是求一个点 $(x_j, y_j)$ 与当前 $k_i$ 构成的 所有直线 中,y轴截距最小

为了方便各位读者理解,在这贴上一幅图,为大家 几何直观 的展示这一过程:

正如华罗庚先生说过的:数缺形时少直观,形少数时难入微,数形结合百般好

Xnip2021-09-25_11-46-08.jpg

图中,黑色点 为所有 $0 \le j \lt i$ 的点 $(x_j,y_j)$,红色线 为 斜率 是 $k_i$ 的 某一条直线

我们 从下往上(截距从小到大) 去逼近当前 所有的点
则 第一个 出现在直线上的点,就是满足 $b_i = y_j - kx_j$ 的 最小截距 $b_i$,即是当前阶段 DP 的 最佳策略

那么,如何有效的维护点集呢?

这就是一个 线性规划 的问题了:在 点集 中,找到一个 点,绘制 固定斜率 的直线,使得 截距最小

由 线性规划 知识可知:我们只用考虑 点集 中 凸壳 上的点即可

几何直观 上,显然这题要维护的是 下凸壳

因此,对于任意的 $f_i$ 来说,我们只需去寻找 下凸壳 上的 点 构成 直线 的 最小截距 即可

这样 时间复杂度 在 最坏 的情况下,还是 $O(n^2)$(即 所有点 的 $(x,y)$ 单增,全部点 构成一个 下凸壳)

斜率优化了个寂寞?并非如此!我们再看看直线方程中,各参数的性质

由于 $t_i, c_i$ 都是 正整数,故他们的 前缀和 $st_i,sc_i$ 是 单调递增的

对应于 直线方程的参数:$x_j = sc_j$ 是 单调递增 的,$k_i = S + st_i$ 也是 单调递增 的

而 下凸壳 中 相邻两点 构成的直线 斜率 也是 单调递增 的

则 下凸壳 中第一个出现在 直线 上的点,满足:$k_{j-1, j} \le k_i \lt k_{j,j+1}$,此时 直线截距 $b_j$ 最小

大家可以配合下图几何直观理解该处的不等式

IMG_C28104BF5784-1.jpeg

而又由于 $k_i$ 单调递增,所以 $j$ 之前 的 点 都不会是 点集 中出现在 直线 上的 第一个点

此时只需 维护点集区间 $[j,i]$ 的 点 即可,直到 $k_{j,j+1} \le k \lt k_{j+1,j+2}$ 时,维护点集区间 变为 $[j+1,i]$

根据上述所说,出现了我们最熟悉的模型-滑动窗口模型。

故我们可以直接利用 单调队列 来维护 下凸壳中的有效点集

并用队头的 两个元素 维护 大于当前斜率 $k_i$ 的最小斜率 $k_{q_{hh}~,~q_{hh+1}}$

这里我把公式展开,方便大家理解:

$$ k_{q_{hh}~,~q_{hh+1}} \gt k_i \Rightarrow \frac{y_{q_{hh+1}}-y_{hh}}{x_{q_{hh+1}}-x_{hh}} \gt k_i \Rightarrow \frac{f_{q_{hh+1}}-f_{hh}}{sc_{q_{hh+1}}-sc_{hh}} \gt S+st_i $$

把点插入 单调队列 前,先要 队列 中 至少有两个点,然后把 满足 $k_{q_{tt-1}~,~q_{tt}} \ge k_{q_{tt}~,~i}$ 的 点 $q_{tt}$ 弹出

即 新加入的点,必须和 原点集 构成 下凸壳,无效点要先删去

这里我把公式展开,方便大家理解:

$$ k_{q_{tt-1}~,~q_{tt}} \lt k_{q_{tt}~,~i} \Rightarrow \frac{y_{q_{tt}}-y_{q_{tt-1}}}{x_{q_{tt}}-x_{q_{tt-1}}} \lt \frac{y_{i}-y_{q_{tt}}}{x_{i}-x_{q_{tt}}} \Rightarrow \frac{f_{q_{tt}}-f_{q_{tt-1}}}{sc_{q_{tt}}-sc_{q_{tt-1}}} \lt \frac{f_{i}-f_{q_{tt}}}{sc_{i}-sc_{q_{tt}}} $$

这样,队列 中 相邻两点 之间构成的直线 斜率单增,也就是我们的 有效下凸壳点集

斜率优化DP模板(来源:Oi-Wiki)

上面密密麻麻零零散散写了一堆分析,最后我们来把 斜率优化 总结成一个 模板,方便大家记忆:

  1. 将初始状态入队
  2. 每次使用一条和 $i$ 相关的直线 $f_i$ 去切维护的凸包,找到最优决策,更新 $dp_i$
  3. 加入状态 $dp_i$。如果一个状态(即凸包上的一个点)在 $dp_i$ 加入后不再是凸包上的点,需要在 $dp_i$ 加入之前剔除

斜率优化往往还要再套一层 二分/CDQ/平衡树优化
不过本题直接用单调队列就好了,上述优化方法的题目会在今后的博客中提到(有生之年)

Code

时间复杂度: $O(n)$

这题非常的好,希望大家不要抄代码,好好推一遍 w

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

typedef long long LL;

const int N = 3e5 + 10;

int n, S, que[N];
LL st[N], sc[N], f[N];

int main()
{
    scanf("%d%d", &n, &S);
    for (int i = 1; i <= n; i ++ )
    {
        scanf("%lld%lld", &st[i], &sc[i]);
        st[i] += st[i - 1], sc[i] += sc[i - 1];
    }

    int hh = 0, tt = 0; que[0] = 0;
    for (int i = 1; i <= n; i ++ )
    {
        while (hh < tt && (f[que[hh + 1]] - f[que[hh]]) <= 
                          (sc[que[hh + 1]] - sc[que[hh]]) * (st[i] + S)) 
            hh ++ ;
        f[i] = f[que[hh]] + S * (sc[n] - sc[que[hh]]) + st[i] * (sc[i] - sc[que[hh]]);
        while (hh < tt && (f[que[tt]] - f[que[tt - 1]]) * (sc[i] - sc[que[tt]]) >=
                          (f[i] - f[que[tt]]) * (sc[que[tt]] - sc[que[tt - 1]]))
            tt -- ;
        que[ ++ tt] = i;
    }
    printf("%lld\n", f[n]);
    return 0;
}

33 评论


用户头像
TallBanana   2023-08-20 14:29      1    踩      回复

彩铅好强,什么都会

用户头像
世末歌者   2023-08-29 14:35      1    踩      回复

看成了彩笔(


用户头像
anss_   2022-02-26 13:16      1    踩      回复

$dalao$,维护队头的时候为什么不能这么写啊,提个负号出来是一样的,为啥就$WA$了😭

while (hh < tt && (f[que[hh]] - f[que[hh + 1]]) <= 
                          (sc[que[hh]] - sc[que[hh + 1]]) * (st[i] + S)) 
            hh ++ ;
用户头像
糖豆   2022-02-28 17:04         踩      回复

<=修改成>=就行了,相当于不等式左右都乘一个-1,这样不等号需要换方向。

用户头像
anss_   2022-02-28 17:08    回复了 糖豆 的评论      1    踩      回复

,,,脑子短路了
谢谢大哥


用户头像
majingxuan123   2025-01-26 16:24         踩      回复

%%%


用户头像
convie   2025-01-05 16:11         踩      回复

再也看不到彩铅发题解了


用户头像
incra   2024-03-31 09:46         踩      回复

注意有些地方写错了

用户头像
incra   2024-03-31 09:46         踩      回复

hh->q[hh]


用户头像
jia_1   2023-05-26 21:12         踩      回复

你是我的神

用户头像
guyushi0831   2023-05-29 21:01         踩      回复

6


用户头像
xyh2   2023-01-12 13:03         踩      回复

真的强


用户头像
大胖子dpz   2022-11-21 23:54         踩      回复

tql


用户头像
acgy   2022-10-06 10:21         踩      回复

请问队列初始化的时候,为什么要先加入一个 0?tt=-1,hh=0。判断条件是 hh <= tt 就过不了。

用户头像
懵逼自动机   2023-04-29 14:50      1    踩      回复

因为也可以从 0 转移过来呀


用户头像
hanbin   2022-09-11 12:22         踩      回复
while (hh < tt && (f[que[hh + 1]] - f[que[hh]]) <= 
                          (sc[que[hh + 1]] - sc[que[hh]]) * (st[i] + S)) 
            hh ++ ;

为啥这里是小于等于,斜率相等的时候说明两直线重和,那最先碰到的点应该就是队头这个点呀,怎么还出队了

用户头像
hanbin   2022-09-11 12:23         踩      回复

我改成<也ac了

用户头像
糖豆   2022-09-28 09:56    回复了 hanbin 的评论         踩      回复

如果直线重合,就是说点集中有两个点与直线方程符合,取哪一个,都可以得到同样的最小截距

用户头像
hanbin   2022-09-28 16:27    回复了 糖豆 的评论      1    踩      回复

但是while循环出队不就把这两个点都出掉了咩

用户头像
糖豆   2022-09-28 17:23    回复了 hanbin 的评论         踩      回复

hh<tt 会保留两个,是不是这样会造成错误呢?我猜的,瞎说的,如果一般情况hh<=tt应该可以,但这里需要保留最少两个

用户头像
hanbin   2022-09-28 20:18    回复了 糖豆 的评论         踩      回复

不太懂你意思

用户头像
一只野生彩色铅笔   2022-09-30 12:04    回复了 hanbin 的评论         踩      回复

可以选择保留,也可以选择不保留


用户头像
做ac梦w   2022-08-24 21:42         踩      回复

想问下图是怎么画的呀


用户头像
CCCkk   2022-08-10 15:57         踩      回复

而又由于 ki 单调递增 错了哦

用户头像
星辰大海_8   2022-09-02 15:37         踩      回复

题主的意思应该是最后形成的图是一个ki单调递增的下凸包,这个是维护形成的不是原来就是


用户头像
种花家的小兔子   2022-07-16 11:21         踩      回复

巨巨,斜率优化是不是基本上都是一维的转移方程?然后再看是否满足条件


用户头像
diandian2020   2022-07-07 15:03         踩      回复

彩铅大佬,请问最后的两个式子里面好像好多地方的下标都应该是 $q_{hh}$ 而非 $hh$ 吧?


用户头像
plank_black   2022-06-29 17:32         踩      回复

%%%


用户头像
霓虹之夜   2022-06-03 20:35         踩      回复

%%%%


用户头像
WA_FOREVER   2022-05-07 15:24         踩      回复

%%%


用户头像
Peter_5   2021-08-06 23:18         踩      回复

好耶, 想明白了!!!


用户头像
abc喵   2021-06-24 18:16         踩      回复

%%%%

用户头像
啾咪   2021-07-08 02:33         踩      回复

%%%++


App 内打开
你确定删除吗?
1024
x

© 2018-2025 AcWing 版权所有  |  京ICP备2021015969号-2
用户协议  |  隐私政策  |  常见问题  |  联系我们
AcWing
请输入登录信息
更多登录方式: 微信图标 qq图标 qq图标
请输入绑定的邮箱地址
请输入注册信息