作为数学渣,先复习一下已知两点\((x_1, y_1)\), \((x_2, y_2)\),怎么求过两点的一次函数的斜率...

待定系数法代入 \(y = kx + b\) 有:

\(x_1k + b = y_1\)

\(x_2k + b = y_2\)

两式相减有:

\(k = \frac{y_2 - y_1}{x_2 - x_1}\)


故事围绕着《算法竞赛进阶指南》的三一道例题展开:

引子

任务安排 1

发现一个关键性质:

假如我们启动了一个任务\([l, r]\),那么它会对后面造成\(S * \sum_{i = r + 1}^{n} C_i\)的费用。

所以我们可以使用费用提前计算的方式优化算法:

设\(st\)为 \(t\) 的前缀和,设\(sum\)为\(C\)的前缀和

\(f[i]\) 表示安排完前 \(i\) 个任务的最小花费:

$f[i] = min(f[j] + (sum[i] - sum[j]) * t[i] + (sum[n] - sum[j]) * S) $

时间复杂度\(O(N ^ 2)\)

情况1. 斜率、横坐标皆单调递增

任务安排 2

将上题推出的转移式子得\(min\)去掉观察:

\(f[i] = f[j] + (sum[i] - sum[j]) * t[i] + (sum[n] - sum[j]) * S\)

发现我们无法优化\(dp\)的原因是有与 \(i, j\) 两者都有关的乘积项,导致我们没有最优策略:

\(- sum[j] * t[i]\)

斜率优化

考虑把这个式子拆开转换为一次函数:\(y = kx + b\) 的形式。

  • 将与 \(i,\ j\) 都有关系的乘积项作为 \(kx\),其中与 \(i\) 有关的作为 \(k\),与 \(j\) 有关的作为 \(x\)
  • 将只与 \(j\) 有关系的值作为 \(y\)
  • 其余的当做 \(b\)

则以上式子可以化成:

\(\underline{f[j]}_y = \underline{(t[i] + S)}_k * \underline{sum[j]}_x + \underline{f[i] - sum[i] * t[i] - sum[n] * S}_b\)

发现当 \(i\) 确定后,该一次函数的斜率 \(k\) 确定,则截距 \(b\) 越小, \(f[i]\) 越小。

我们将 \((x, y)\) 即 \((sum[j], f[j])\) 放在坐标系上。

则形象化可理解为一条直线从下往上平移,所碰到的第一个点即为最优解。

发现一个点如果被另外两个点围起来,永远不可能作为最优解。

删除了这些点后,发现相邻点之间的斜率为单调递增的,即构成一个凸包:

发现一个斜率 \(k\) 固定的直线所匹配的最优点满足:

  • 其右边的斜率都 $ > k$
  • 其左边的斜率都 \(< k\)

由于这道题斜率 \(t[i] + S\)、横坐标 \(sum[j]\) 皆单调递增。

  1. 由于横坐标递增,所以维护凸包时,每当加入一个点时:
  • 若上面两个点构成的斜率大于这个点和上一个点的斜率,即不满足单调性,可以弹出队尾。即:

    \(\frac{y_{q[tt]} - y_{q[tt - 1]}}{x_{q[tt]} - x_{q[tt - 1]}} >= \frac{y_{i} - y_{q[tt]}}{x_{i} - x_{q[tt]}}\)

  1. 由于斜率递增,所以 \(i + 1\) 的最优解一定在 \(i\) 的右边,所以一旦队头两个点构成的斜率 $ < $ 当前的斜率,可以弹出队头。即满足:

    \(\frac{y_{q[hh + 1]} - y_{q[hh]}}{x_{q[hh + 1]} - x_{q[hh]}} < t[i] + S\)

然后队头的元素即为最优选择。

时间复杂度 \(O(N)\)

\(Tips:\)

  1. 由于除法会有精度问题,可以通过交叉相乘的形式比较大小
#include <cstring>
#include <cstdio>
#include <iostream>
#define x(a) (c[a])
#define y(a) (f[a])
#define k(a) (t[a] + S)
using namespace std;
typedef long long LL;
const int N = 300005;
int n, S;
LL t[N], c[N], q[N], f[N];
int main() {
scanf("%d%d", &n, &S);
for (int i = 1; i <= n; i++) scanf("%lld%lld", t + i, c + i);
for (int i = 1; i <= n; i++) t[i] += t[i - 1], c[i] += c[i - 1];
int hh = 0, tt = 0;
q[0] = 0;
for (int i = 1; i <= n; i++) {
while(hh < tt && (y(q[hh + 1]) - y(q[hh])) <= ((t[i] + S) * (x(q[hh + 1]) - x(q[hh])))) hh++;
f[i] = f[q[hh]] + (c[i] - c[q[hh]]) * t[i] + (c[n] - c[q[hh]]) * S;
while(hh < tt && ((y(q[tt]) - y(q[tt - 1])) * (x(i) - x(q[tt])) >= ((y(i) - y(q[tt])) * (x(q[tt]) - x(q[tt - 1]))))) tt--;
q[++tt] = i;
}
printf("%lld\n", f[n]);
return 0;
}

情况2. 横坐标单调递增

任务安排3

此时的斜率不再递增了,也就是我们不能\(pop\_front\)了,不过我们仍可以维护凸包,然后保持单调性,二分。

时间复杂度\(O(Nlog_2N)\)

#include <cstring>
#include <cstdio>
#include <iostream>
#define x(a) (c[a])
#define y(a) (f[a])
#define k(a) (t[a] + S)
using namespace std;
typedef long long LL;
const int N = 300005;
int n, S, t[N], c[N], q[N];
LL f[N];
int main() {
scanf("%d%d", &n, &S);
for (int i = 1; i <= n; i++) scanf("%lld%lld", t + i, c + i);
for (int i = 1; i <= n; i++) t[i] += t[i - 1], c[i] += c[i - 1];
int hh = 0, tt = 0;
q[0] = 0;
for (int i = 1; i <= n; i++) {
int l = hh, r = tt;
while(l < r) {
int mid = (l + r) >> 1;
if((y(q[mid + 1]) - y(q[mid])) >= ((LL)k(i) * (x(q[mid + 1]) - x(q[mid])))) r = mid;
else l = mid + 1;
}
f[i] = f[q[r]] + (LL)(c[i] - c[q[r]]) * t[i] + (LL)(c[n] - c[q[r]]) * S;
while(hh < tt && ((y(q[tt]) - y(q[tt - 1])) * (x(i) - x(q[tt])) >= ((y(i) - y(q[tt])) * (x(q[tt]) - x(q[tt - 1]))))) tt--;
q[++tt] = i;
}
printf("%lld\n", f[n]);
return 0;
}

例题

运输小猫

设 \(d[i]\) 为从 \(1\) 走到 \(i\) 的距离。

那么每条小猫最佳的出发时间应为 \(a[i] = t[i] - d[h[i]]\),如果要接上这只猫,必须大于这个时间出发。

我们将 \(a\) 数组排序,那么问题等价转换于把 \(m\) 个点划分成 \(p\) 个连续区间,使每一段到右端点的距离之和的总和最小。(内心 \(OS\):这不就是摆渡车的变种吗?)

那么朴素 \(dp\) 便很好列出了:

\(f[k][i]\) 表示将前 \(i\) 只小猫分成 \(k\) 组的最小总和。

设 \(sumA\) 为 \(a\) 数组的前缀和。

\(f[k][i] = min(f[k - 1][j] + a[i] * (i - j) - sumA[i] + sumA[j]) (0 <= j < i)\)


由于这里面有一个非常讨厌的 \(a[i] * -j\),所以我们考虑斜率优化:

\(\underline{f[k - 1][j] + sumA[j]}_y = \underline{a[i]}_k * \underline{j}_x + \underline{f[k][i] - a[i] * i + sumA[i]}_b\)

发现这里的横坐标、斜率都是单调递增,即情况 \(1\)。那么我们可以将不需要的直接踢出即可。

时间复杂度 \(O(PM)\)

#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
typedef long long LL;
const int N = 100005, S = 105;
int n, m, P, d[N], a[N], q[N];
LL f[S][N], sum[N];
LL inline y(int i, int k) {
return f[k - 1][i] + sum[i];
}
int main() {
memset(f, 0x3f, sizeof f);
scanf("%d%d%d", &n, &m, &P);
for (int i = 0; i <= P; i++) f[i][0] = 0;
for (int i = 2; i <= n; i++)
scanf("%d", d + i), d[i] += d[i - 1]; for (int i = 1, h, t; i <= m; i++) {
scanf("%d%d", &h, &t); a[i] = t - d[h];
}
sort(a + 1, a + 1 + m);
for (int i = 1; i <= m; i++) sum[i] = sum[i - 1] + a[i];
for (int k = 1; k <= P; k++) {
int hh = 0, tt = 0;
for (int i = 1; i <= m; i++) {
while (hh < tt && (y(q[hh + 1], k) - y(q[hh], k)) <= (LL)a[i] * (q[hh + 1] - q[hh])) hh++;
f[k][i] = f[k - 1][q[hh]] + (LL)a[i] * (i - q[hh]) - (sum[i] - sum[q[hh]]);
while (hh < tt && (y(q[tt], k) - y(q[tt - 1], k)) * (i - q[tt]) >= (y(i, k) - y(q[tt], k)) * (q[tt] - q[tt - 1])) tt--;
q[++tt] = i;
}
}
printf("%lld\n", f[P][m]);
}

学习笔记:斜率优化DP的更多相关文章

  1. 学习笔记·斜率优化 [HNOI2008]玩具装箱

    \(qwq\)今天\(rqy\)给窝萌这些蒟蒻讲了斜率优化--大概是他掉打窝萌掉打累了吧顺便偷了\(rqy\)讲课用的图 \(Step \ \ 1\) 一点小转化 事实上斜率优化是专门用来处理这样一类 ...

  2. 算法笔记--斜率优化dp

    斜率优化是单调队列优化的推广 用单调队列维护递增的斜率 参考:https://www.cnblogs.com/ka200812/archive/2012/08/03/2621345.html 以例1举 ...

  3. 【学习笔记】动态规划—斜率优化DP(超详细)

    [学习笔记]动态规划-斜率优化DP(超详细) [前言] 第一次写这么长的文章. 写完后感觉对斜优的理解又加深了一些. 斜优通常与决策单调性同时出现.可以说决策单调性是斜率优化的前提. 斜率优化 \(D ...

  4. 斜率优化DP学习笔记

    先摆上学习的文章: orzzz:斜率优化dp学习 Accept:斜率优化DP 感谢dalao们的讲解,还是十分清晰的 斜率优化$DP$的本质是,通过转移的一些性质,避免枚举地得到最优转移 经典题:HD ...

  5. 【笔记篇】斜率优化dp(一) HNOI2008玩具装箱

    斜率优化dp 本来想直接肝这玩意的结果还是被忽悠着做了两道数论 现在整天浑浑噩噩无心学习甚至都不是太想颓废是不是药丸的表现 各位要知道我就是故意要打删除线并不是因为排版错乱 反正就是一个del标签嘛并 ...

  6. APIO2010 特别行动队 & 斜率优化DP算法笔记

    做完此题之后 自己应该算是真正理解了斜率优化DP 根据状态转移方程$f[i]=max(f[j]+ax^2+bx+c),x=sum[i]-sum[j]$ 可以变形为 $f[i]=max((a*sum[j ...

  7. 动态规划专题(五)——斜率优化DP

    前言 斜率优化\(DP\)是难倒我很久的一个算法,我花了很长时间都难以理解.后来,经过无数次的研究加以对一些例题的理解,总算啃下了这根硬骨头. 基本式子 斜率优化\(DP\)的式子略有些复杂,大致可以 ...

  8. hdu3507Print Article(斜率优化dp)

    Print Article Time Limit: 9000/3000 MS (Java/Others)    Memory Limit: 131072/65536 K (Java/Others)To ...

  9. HDU-3507Print Article 斜率优化DP

    学习:https://blog.csdn.net/bill_yang_2016/article/details/54667902 HDU-3507 题意:有若干个单词,每个单词有一个费用,连续的单词组 ...

  10. 一本通提高篇——斜率优化DP

    斜率优化DP:DP的一种优化形式,主要用于优化如下形式的DP f[i]=f[j]+x[i]*x[j]+... 学习可以参考下面的博客: https://www.cnblogs.com/Xing-Lin ...

随机推荐

  1. 154. Find Minimum in Rotated Sorted Array II(循环数组查找)

    Suppose an array sorted in ascending order is rotated at some pivot unknown to you beforehand. (i.e. ...

  2. linux文件cat/tac/more/less/head/tail/find/vimdiff

    ls查看目录文件里的文件: [root@localhost test]# ls a  aa  b  c -d选项查看目录文件自身信息: [root@localhost test]# ll -d drw ...

  3. MYSQL学习(三) --索引详解

    创建高性能索引 (一)索引简介 索引的定义 索引,在数据结构的查找那部分知识中有专门的定义.就是把关键字和它对应的记录关联起来的过程.索引由若干个索引项组成.每个索引项至少包含两部分内容.关键字和关键 ...

  4. 计算机&编程语言发展史

    计算机&编程语言发展史 编辑于2020-11-18 计算机的基本组成 计算机的发展经历了哪几代? 第一代 电子管计算机 第二代 晶体管计算机 第三代 集成电路计算机 第四代 大规模和超大规模集 ...

  5. 企业级工作流解决方案(十一)--集成Abp和ng-alain--权限系统服务

    权限系统主要定义为管理员增删改查权限数据,直接读取数据库,权限系统服务主要定义为供其他系统调用的权限验证接口,定义为两个不同的微服务. 权限系统有一个特点,数据变动比较小,数据量本身并不是很大,访问量 ...

  6. 基于gin的golang web开发:永远不要相信用户的输入

    作为后端开发者我们要记住一句话:"永远不要相信用户的输入",这里所说的用户可能是人,也可能是另一个应用程序."永远不要相信用户的输入"是安全编码的准则,也就是说 ...

  7. 这份SpringMVC执行原理笔记,建议做java开发的好好看看,总结的很详细!

    什么是SpringMVC? Spring MVC属于SpringFrameWork的后续产品,已经融合在Spring Web Flow里面.Spring 框架提供的web模块,包含了开发Web 应用程 ...

  8. Java基础教程——缓冲流

    缓冲流 "缓冲流"也叫"包装流",是对基本输入输出流的增强: 字节缓冲流: BufferedInputStream , BufferedOutputStream ...

  9. IEEE754标准浮点数表示与舍入

    原文地址:https://blog.fanscore.cn/p/26/ 友情提示:本文排版不太好,但内容简单,请耐心观看,总会搞懂的. 1. 定点数 对于一个无符号二进制小数,例如101.111,如果 ...

  10. devc++编译时 undefined reference to `__imp_WSAStartup'

    socket编程时遇到的问题: