我们以一道例题引入:

洛谷 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. java高级用法之:JNA中的回调

    目录 简介 JNA中的Callback callback的应用 callback的定义 callback的获取和应用 在多线程环境中使用callback 总结 简介 什么是callback呢?简单点说 ...

  2. Vue3 与依赖注入

    Vue3 与依赖注入 本文写于 2021 年 2 月 19 日 在 React 中,我们可以通过 context 与 useContext 实现单例.注入--等诸多特性. 详细请看上一篇文章:如何利用 ...

  3. GO 语言入门(一)

    GO 语言入门(一) 本文写于 2020 年 1 月 18 日 Go 由 Google 工程师 Robert Griesemer,Rob Pike 和 Ken Thompson 设计的一门编程语言,第 ...

  4. 单例模式与pickle模块

    目录 设计模式之单例模式 pickle模块 设计模式之单例模式 设计模式是前辈们发明的经过反复验证用于解决固定问题的固定套路,在IT行业中设计模式总共有23种,可以分为三大类:创建型.结构型.行为型. ...

  5. HMS Core分析服务6.5.0版本更新啦

    卸载用户价值的合理评估对制定相应的用户召回策略具有重要意义. HMS Core分析服务新版本支持查看用户卸载前使用次数.崩溃次数等指标.通过这些数据,您可以更直观地判断已卸载人群粘性以及崩溃问题对用户 ...

  6. Vue出现Component template should ...

    当运行vue出现错误Component template should contain exactly one root element. If you ...的时候,我们只需要将<templa ...

  7. 代码模板整理(0):先水一波a+b

    梦开始的地方 "Hello World!" #include<bits/stdc++.h> using namespace std; int main(){ cout& ...

  8. 【物联网串口服务器通信经验教程】Modbus网关协议转换

    在前面的文章中,我们已经详细地介绍了Modbus网关的几种主要类型,今天,就让我们来介绍一下其中简单协议转换的处理过程. 简单协议转换是最常规.最普遍的Modbus网关功能,也是数据处理效率最高Mod ...

  9. Spring Boot 实践 :Spring Boot + MyBatis

    Spring Boot 实践系列,Spring Boot + MyBatis . 目的 将 MyBatis 与 Spring Boot 应用程序一起使用来访问数据库. 本次使用的Library spr ...

  10. 一文掌握软件安全必备技术 SAST

    上一篇文章中,我们讨论了软件供应链的概念并了解到近年来软件供应链安全事件层出不穷.为了保障软件供应链安全,我们需要了解网络安全领域中的一些主要技术.本篇文章将介绍其中一个重要技术--SAST. 当开发 ...