我们以一道例题引入:

洛谷 P2365 任务安排:


\(n\) 个任务排成一个序列在一台机器上等待完成(顺序不得改变),这 \(n\) 个任务被分成若干批,每批包含相邻的若干任务。

从零时刻开始,这些任务被分批加工,第 \(i\) 个任务单独完成所需的时间为 \(t_i\)​。在每批任务开始前,机器需要启动时间 \(s\),而完成这批任务所需的时间是各个任务需要时间的总和(同一批任务将在同一时刻完成)。

每个任务的费用是它的完成时刻乘以一个费用系数 \(C_i\)​。请确定一个分组方案,使得总费用最小。

设 \(dp_i\) 为选取到第 \(i\) 个任务时的最大价值,枚举一个起点 \(j\) 分批可以得到:

\[dp_i=\min\{dp_i,dp_j+\text{sum}_T(1,i)\text{sum}_C(i,j+1)+s\cdot\text{sum}_C(n,j+1)\}
\]

其中对于序列 \(S\),

\[\text{sum}_S(l,r)=\sum_{i=l}^rS_i=\text{sumS}_r-\text{sumS}_{l-1}
\]

后面这个等号是求法,\(\rm sumS\) 是前缀和,也就是

\[\text{sumS}_i=\sum_{k=1}^iS_i
\]

时间复杂度 \(O(n^2)\) .

Code:

#include<ctime>
#include<queue>
#include<stack>
#include<cmath>
#include<iterator>
#include<cctype>
#include<vector>
#include<map>
#include<set>
#include<algorithm>
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<bitset>
using namespace std;
const int N=5005;
int n,s,t[N],c[N],sumT[N],sumC[N],dp[N];
int main()
{
scanf("%d%d",&n,&s);
for (int i=1;i<=n;i++)
{
scanf("%d%d",t+i,c+i);
sumT[i]=sumT[i-1]+t[i]; sumC[i]=sumC[i-1]+c[i];
} memset(dp,0x3f,sizeof dp); dp[0]=0;
for (int i=1;i<=n;i++)
for (int j=0;j<i;j++)
dp[i]=min(dp[i],dp[j]+sumT[i]*(sumC[i]-sumC[j])+s*(sumC[n]-sumC[j]));
printf("%d",dp[n]);
return 0;
}

这个 \(O(n^2)\) 的做法还是太慢了,过不了 \(3\times 10^5\) 的数据,考虑优化。

要优化,当然先对动态转移方程变形:

\[\begin{aligned}dp_i&=\min\{dp_i,dp_j+\text{sum}_T(1,i)\text{sum}_C(i,j+1)+s\cdot\text{sum}_C(n,j+1)\}\\&=dp_j-(\text{sumT}_i+s)\text{sumC}_j+\text{sumT}_i\text{sumC}_i+s\cdot\text{sumC}_n&\text{设 }j\text{ 是使得 }dp_i \text{ 取到最小值的 }j\\\Rightarrow dp_j&=(\text{sumT}_i+s)\text{sumC}_j+dp_i-\text{sumT}_i\text{sumC}_i-s\cdot\text{sumC}_n\end{aligned}
\]

此时令 \(\begin{cases}dp_j=y&\text{因变量}\\\text{sumT}_i+s=k&\text{斜率}\\\text{sumC}_j=x&\text{自变量}\\dp_i-\text{sumT}_i\text{sumC}_i-s\cdot\text{sumC}_n=b&\text{截距}\end{cases}\) 可以得到:

\[y=kx+b
\]

发现这正好是一个直线方程,并且 \(x\) 是随着 \(j\) 改变而改变的。

别忘了我们的目标:使 \(dp_i\) 最小,要使 \(dp_i\) 最小,就要让 \(b\)(截距)最小(\(b\) 中除了 \(dp_i\) 以外的东西都是常量)。

我们可以在平面直角坐标系上点出下列点:\((dp_0,\text{sumC}_0),(dp_1,\text{sumC}_1),\cdots,(dp_{i-1},\text{sumC}_{i-1})\) 还有斜率 \(k\) 表示的直线:

P.S. 以下所有图的横坐标都是 \(\text{sumC}_j\),纵坐标都是 \(dp_j\) .

我们将 \(b\) 改变,直线将会滑动:

注意最小的 \(j\) 正好就是滑动时第一次遇到的点。

每次更新的时候后面都会插入点,注意到斜率 \(k\) 和插入点的横坐标都是 递增 的,所以我们可以将相邻两点的直线连上,然后把上面的点全部去掉(因为不可能会成为最小的 \(j\) 了):

发现绿线那里正好连成了一个下凸壳(凸包的定义:一个多边形是凸包当且仅当对于它的所有边满足所有点都在它所在的直线的一侧)。

现在我们怎么找遇到的第一个点呢?

注意到凸包相邻两点间的的斜率是递增的,手玩或者找规律可以得到答案就是 第一个斜率 \(>k\) 的点 所以我们可以二分。

当然,我们还有一种办法。

这个问题相当于在一个单调队列中找第一个大于 \(k\) 的点。

策略 \(1\):在查询的时候,可以把队头小于当前斜率的点全部删掉(因为不可能会参与答案了)

策略 \(2\):在插入的时候,将队尾所有不在凸包(不满足凸包性质)的点全部删掉(不要删掉插入进去的点)

不满足凸包性质的判断是纵坐标高于两点并且横坐标在两点之间,比如下图中,加入新点 \(N\),这使得 \(G\) 不满足性质,应当删去。

就按这写代码即可,注意条件:

  • 策略 \(1\):\(\dfrac{dp_2-dp_1}{C_2-C_1}\le \text{sumT}_i+s\)
  • 策略 \(2\):\(\dfrac{dp_{tail}-dp_{tail-1}}{C_{tail}-C_{tail-1}}\ge\dfrac{dp_{i}-dp_{tail}}{C_{i}-C_{tail}}\)(其中 \(tail\) 是队尾)

上面是按斜率式写的,应该很容易理解,但是代码里应该注意式子有除法要移项变成乘法减少误差。

Code:

#include<ctime>
#include<queue>
#include<stack>
#include<cmath>
#include<iterator>
#include<cctype>
#include<vector>
#include<map>
#include<set>
#include<algorithm>
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<bitset>
using namespace std;
const int N=3e5+5;
typedef long long ll; // 开 long long
int n,s,t[N],c[N];
ll sumT[N],sumC[N],dp[N],q[N];
int main()
{
scanf("%d%d",&n,&s);
for (int i=1;i<=n;i++)
{
scanf("%d%d",t+i,c+i);
sumT[i]=sumT[i-1]+t[i]; sumC[i]=sumC[i-1]+c[i]; // 前缀和
} int head=0,tail=0; // q[0]=0; 这句是隐式的,不用写
for (int i=1;i<=n;i++)
{
while ((head<tail)&&(dp[q[head+1]]-dp[q[head]]<=(sumT[i]+s)*(sumC[q[head+1]]-sumC[q[head]]))) ++head; // 策略 1
int j=q[head];
dp[i]=dp[j]+sumT[i]*(sumC[i]-sumC[j])+s*(sumC[n]-sumC[j]); // 转移,此时的 j(也就是 q[head])已经是最小的 j 了,所以不用加 min 了
while ((head<tail)&&((dp[q[tail]]-dp[q[tail-1]])*(sumC[i]-sumC[q[tail]])>=
(dp[i]-dp[q[tail]]) *(sumC[q[tail]]-sumC[q[tail-1]]))) --tail; // 策略 2
q[++tail]=i; // 最后再入队(因为策略 2 里不能把插入的点丢出去)
}
printf("%lld",dp[n]);
return 0;
}



如果像 SDOI2012 任务安排 那样,\(T_i\) 是负数(时 间 倒 流),那么单调性就没了,就只能二分了 qwq

将原代码里的

while ((head<tail)&&(dp[q[head+1]]-dp[q[head]]<=(sumT[i]+s)*(sumC[q[head+1]]-sumC[q[head]]))) ++head;
int j=q[head];

换成

int l=head,r=tail;
while (l<r)
{
int mid=(l+r)>>1;
if (dp[q[mid+1]]-dp[q[mid]]>(sumT[i]+s)*(sumC[q[mid+1]]-sumC[q[mid]])) r=mid;
else l=mid+1;
} int j=q[r];

即可。

那么如果 \(C_i\) 是负数呢?

可以倒序 dp,设计一个状态转移方程,让 \(\text{sumT}_i\) 是横坐标,\(\text{sumC}_i\) 是斜率中的一项。仍然可以用单调队列维护凸壳,用二分法求出最优决策。

那么如果 \(T_i,C_i\) 都是负数呢?

可以考虑 cdq 分治或平衡树维护凸包,具体可以参考 NOI2007 货币兑换或者 OI-Wiki


总结一下,斜率优化是解决形如

\[dp_i=\min_{L(i)\le j\le R(i)}\{dp_j+S(i,j)\}
\]

其中 \(L(i),R(i)\) 是关于 \(i\) 的一次函数,\(S(i,j)\) 是关于 \(i,j\) 的多项式(可以有 \(i,j\) 的乘积项)。

(这好像也叫 1D/1D 型转移?)

还有一个特点:斜率优化的转移方程一般很复杂 qwq


习题:Codeforces 311B Cats Transport

斜率优化 dp 总结的更多相关文章

  1. bzoj-4518 4518: [Sdoi2016]征途(斜率优化dp)

    题目链接: 4518: [Sdoi2016]征途 Description Pine开始了从S地到T地的征途. 从S地到T地的路可以划分成n段,相邻两段路的分界点设有休息站. Pine计划用m天到达T地 ...

  2. bzoj-1096 1096: [ZJOI2007]仓库建设(斜率优化dp)

    题目链接: 1096: [ZJOI2007]仓库建设 Description L公司有N个工厂,由高到底分布在一座山上.如图所示,工厂1在山顶,工厂N在山脚.由于这座山处于高原内陆地区(干燥少雨),L ...

  3. [BZOJ3156]防御准备(斜率优化DP)

    题目:http://www.lydsy.com:808/JudgeOnline/problem.php?id=3156 分析: 简单的斜率优化DP

  4. 【BZOJ-1096】仓库建设 斜率优化DP

    1096: [ZJOI2007]仓库建设 Time Limit: 10 Sec  Memory Limit: 162 MBSubmit: 3719  Solved: 1633[Submit][Stat ...

  5. BZOJ 1010: [HNOI2008]玩具装箱toy 斜率优化DP

    1010: [HNOI2008]玩具装箱toy Description P教授要去看奥运,但是他舍不下他的玩具,于是他决定把所有的玩具运到北京.他使用自己的压缩器进行压缩,其可以将任意物品变成一堆,再 ...

  6. BZOJ 3156: 防御准备 斜率优化DP

    3156: 防御准备 Description   Input 第一行为一个整数N表示战线的总长度. 第二行N个整数,第i个整数表示在位置i放置守卫塔的花费Ai. Output 共一个整数,表示最小的战 ...

  7. HDU2829 Lawrence(斜率优化dp)

    学了模板题之后上网搜下斜率优化dp的题目,然后就看到这道题,知道是斜率dp之后有思路就可以自己做不出来,要是不事先知道的话那就说不定了. 题意:给你n个数,一开始n个数相邻的数之间是被东西连着的,对于 ...

  8. HDU3507 Print Article(斜率优化dp)

    前几天做多校,知道了这世界上存在dp的优化这样的说法,了解了四边形优化dp,所以今天顺带做一道典型的斜率优化,在百度打斜率优化dp,首先弹出来的就是下面这个网址:http://www.cnblogs. ...

  9. HDU 3507 Print Article(斜率优化DP)

    题目链接 题意 : 一篇文章有n个单词,如果每行打印k个单词,那这行的花费是,问你怎么安排能够得到最小花费,输出最小花费. 思路 : 一开始想的简单了以为是背包,后来才知道是斜率优化DP,然后看了网上 ...

  10. 斜率优化dp(POJ1180 Uva1451)

    学这个斜率优化dp却找到这个真心容易出错的题目,其中要从n倒过来到1的确实没有想到,另外斜率优化dp的算法一开始看网上各种大牛博客自以为懂了,最后才发现是错了. 不过觉得看那些博客中都是用文字来描述, ...

随机推荐

  1. 在博客文章中使用mermaid 定义流程图,序列图,甘特图

    概述 Mermaid(美人鱼)是一套markdown语法规范,用来在markdown文档中定义图形,包括流程图.序列图.甘特图等等. 它的官方网站是 https://mermaid-js.github ...

  2. axios源码解析 - 请求方法的别名实现

    axios中的创建请求方式很多,比如axios(url),axios.get(url),axios.post(url),axios.delete(url),方便快捷的api设计让axios火得一塌糊涂 ...

  3. python之贪婪算法

    贪婪算法 贪婪算法也称为最优算法,这种算法并不是最准确的答案,但确认最接近答案的近似算法. 这时候有人会问,不是最准确的答案我要她干嘛?但是在日常中,我们有时候会遇到一些我们无法处理的问题,甚至是要花 ...

  4. CentOS6.x静默安装Oracle12c

    一.准备 1.1 安装环境 系统要求 内存 > 2G 安装目录空间 > 6.5G /tmp目录空间 > 1G 操作系统 cat /etc/redhat-release 用rpm命令确 ...

  5. 好客租房30-事件绑定this指向(箭头函数)

    1箭头函数 利用箭头函数自身不绑定this的特点 //导入react     import React from 'react'           import ReactDOM from 'rea ...

  6. 第24章 Java 数据类型转换

    每日一句 井底点灯深烛伊,共郎长行莫围棋. 每日一句 What we call "failure" is not falling down, but the staying dow ...

  7. TypeError: this.getOptions is not a function

    我在vue ui界面中安装版本依赖包后报这个错误 less-loader/sass-loader安装的版本过高 解决办法 删除原有的版本依赖包,安装更低版本的依赖包. 如 @6.0.1为选择安装的版本 ...

  8. Go中rune类型浅析

    一.字符串简单遍历操作 在很多语言中,字符串都是不可变类型,golang也是. 1.访问字符串字符 如下代码,可以实现访问字符串的单个字符和单个字节 package main import ( &qu ...

  9. MySQL - 数据库的隔离级别

    MySQL - 数据库的隔离级别 隔离级别 脏读(Dirty Read) 不可重复读(NonRepeatable Read) 幻读(Phantom Read) 未提交读(Read uncommitte ...

  10. 我所使用的生产 Java 17 启动参数

    JVM 参数升级提示工具:jacoline.dev/inspect JVM 参数词典:chriswhocodes.com Revolut(英国支付巨头)升级 Java 17 实战:https://ww ...