当前位置: 首页 > news >正文

【动态规划】5 从一次函数出发推导斜率优化dp

背景

基于例题《任务安排》逐步推导进行斜率优化。

引入

例题:P2365 任务安排

考虑动态规划。使用 d p i , j dp_{i,j} dpi,j 表示前 i i i 个任务分了 j j j 段的最小费用。

显然,有 d p i , j = min ⁡ k = 1 i − 1 ( d p i , j , d p k , j − 1 + ( t o t i − t o t k ) ) ∗ ( s u m [ i ] + s ∗ j ) ) dp_{i,j} = \min_{k=1}^{i-1} (dp_{i,j},dp_{k,j-1} + (tot_i-tot_k))*(sum[i]+s*j)) dpi,j=mink=1i1(dpi,j,dpk,j1+(totitotk))(sum[i]+sj))​ 。

  • s u m i sum_i sumi 表示 c i c_i ci 的前缀和。
  • t o t i tot_i toti 表示 t i t_i ti 的前缀和。

前缀和优化后,时间复杂度 O ( n 3 ) O(n^3) O(n3),得到 60pts.

代码

#include <bits/stdc++.h>
using namespace std;
int n,s,ans,t[5005],c[5005],dp[5005][5005],sum[5005],tot[5005];
int main()
{cin >> n >> s;for (int i=1;i<=n;i++){cin >> t[i] >> c[i];sum[i] = sum[i-1] + t[i];tot[i] = tot[i-1] + c[i];}memset(dp,0x3f,sizeof(dp));ans = 0x3f3f3f3f;dp[0][0] = 0;for (int i=1;i<=n;i++){for (int j=1;j<=i;j++){for (int k=0;k<i;k++){dp[i][j] = min(dp[i][j],dp[k][j-1] + (tot[i]-tot[k])*(sum[i]+s*j));}	}	}for (int i=1;i<=n;i++){ans = min(ans,dp[n][i]);}cout<<ans;return 0;
}

如何进一步优化呢?

我们发现,可以把有关 s s s 的计算在前面完成。也就是 费用提前计算 ,就不需要枚举分的段数了。

得到状态转移方程 d p i = min ⁡ ( d p i , d p j + s u m i ∗ t o t i − s u m i ∗ t o t j + t o t n ∗ s − t o t j ∗ s ) dp_i = \min(dp_i,dp_j + sum_i*tot_i-sum_i*tot_j + tot_n*s-tot_j*s) dpi=min(dpi,dpj+sumitotisumitotj+totnstotjs)

代码

#include <bits/stdc++.h>
using namespace std;
long long n,s,ans,t[5005],c[5005],dp[5005],sum[5005],tot[5005];
int main()
{cin >> n >> s;for (int i=1;i<=n;i++){cin >> t[i] >> c[i];sum[i] = sum[i-1] + t[i];tot[i] = tot[i-1] + c[i];dp[i] = 1e18;}ans = 1e18;dp[0] = 0;for (int i=1;i<=n;i++){for (int j=0;j<i;j++){dp[i] = min(dp[i],dp[j] + sum[i]*(tot[i]-tot[j]) + (tot[n]-tot[j])*s);}	}cout<<dp[n];return 0;
}

正文

状态转移方程 d p i = min ⁡ ( d p i , d p j + s u m i ∗ t o t i − s u m i ∗ t o t j + t o t n ∗ s − t o t j ∗ s ) dp_i = \min(dp_i,dp_j + sum_i*tot_i-sum_i*tot_j + tot_n*s-tot_j*s) dpi=min(dpi,dpj+sumitotisumitotj+totnstotjs)

把与 i , j i,j i,j 有关的各单独放在一起,得到 d p i = min ⁡ ( d p i , d p j + s u m i ∗ t o t i + t o t n ∗ s − t o t j ∗ ( s u m i + s ) ) dp_i = \min(dp_i,dp_j + sum_i*tot_i + tot_n*s - tot_j*(sum_i+s)) dpi=min(dpi,dpj+sumitoti+totnstotj(sumi+s))

去掉最小值,得到 d p i = d p j + s u m i ∗ t o t i + t o t n ∗ s − t o t j ∗ ( s u m i + s ) dp_i = dp_j + sum_i*tot_i + tot_n*s - tot_j*(sum_i+s) dpi=dpj+sumitoti+totnstotj(sumi+s)

移项,得到 d p j = t o t j ∗ ( s u m i + s ) + d p i − s u m i ∗ t o t i − t o t n ∗ s dp_j = tot_j*(sum_i+s) + dp_i - sum_i*tot_i - tot_n*s dpj=totj(sumi+s)+dpisumitotitotns

t o t j tot_j totj 为横坐标, d p j dp_j dpj 为纵坐标的平面直角坐标系中,

这是一条 y = ( s + s u m i ) ∗ x + d p i − s u m i ∗ t o t i − t o t n ∗ s y = (s+sum_i) * x + dp_i - sum_i * tot_i - tot_n * s y=(s+sumi)x+dpisumitotitotns 的直线。

写成 y = k x + b y = kx+b y=kx+b 的形式, k = s + s u m i k = s+sum_i k=s+sumi b = d p i − s u m i ∗ t o t i − t o t n ∗ s b = dp_i-sum_i*tot_i-tot_n*s b=dpisumitotitotns.

由于 k k k 是定值,所求的 d p i dp_i dpi 存在于 b b b 中,所以我们只需要找到最小的 b b b 即可。


如何寻找最小的 b b b

发现有一些点会出现在这条直线上,我们把这样的点称为 决策点,即 ( t o t j , d p j ) (tot_j,dp_j) (totj,dpj)

对于这些决策点,由于 k k k 是定值,所以有且只有一条 k = s + s u m i k=s+sum_i k=s+sumi 的直线经过一个决策点,这些决策点一共会产生不超过 j j j 条直线。

对于已知的一个决策点 ( t o t j , d p j ) (tot_j,dp_j) (totj,dpj),我们把它们带入到一次函数表达式里去,就能解出一个 b b b ,枚举 j j j 得到最小的 b b b 即可。

但这种方法过于朴素,时间复杂度不变。考虑优化。

由于我们是从决策点出发,推导 b b b 的值。则说明决策点坐标(或者说 j j j )与 b b b 之间存在线性关系。考虑决策点坐标之间的关系来优化。

对于三个决策点 ( t o t j 1 , d p j 1 ) , ( t o t j 2 , d p j 2 ) , ( t o t j 3 , d p j 3 ) (tot_{j_1},dp_{j_1}),(tot_{j_2},dp_{j_2}),(tot_{j_3},dp_{j_3}) (totj1,dpj1),(totj2,dpj2),(totj3,dpj3) (我们设这三点 j 1 < j 2 < j 3 j_1 < j_2 < j_3 j1<j2<j3 ,由于 t , c > 0 t,c > 0 t,c>0 ,所以这三点的横坐标依次递增,即 t o t j 1 < t o t j 2 < t o t j 3 tot_{j_1} < tot_{j_2} < tot_{j_3} totj1<totj2<totj3)来说,当这三个决策点有且仅有取 ( t o t j 2 , d p j 2 ) (tot_{j_2},dp_{j_2}) (totj2,dpj2) 时, b b b 有最小值,那么这三点所构成的直线不会两两重合,并分为两种情况:

情况 1 ( j 2 j_2 j2 j 1 j_1 j1 j 3 j_3 j3 的连线上方)

当这三点构成一个向上凸出的形状,即 上凸 。显然此时 j 2 j_2 j2 一定不会使得 b b b 取最小值,如下图所示。

image.png

情况 2 ( j 2 j_2 j2 j 1 j_1 j1 j 3 j_3 j3 的连线下方)

当这三点构成一个向下凸出的形状,即 下凸 。显然此时 j 2 j_2 j2 可能使得 b b b 取最小值,如下图所示。

image.png

发现只有 下凸 的情况 ( j 2 j_2 j2 j 1 j_1 j1 j 3 j_3 j3 的连线下方) 才可能使 j 2 j_2 j2 取到最小的 b b b

则有 d p j 2 − d p j 1 t o t j 2 − t o t j 1 < d p j 3 − d p j 2 t o t j 3 − t o t j 2 \frac{dp_{j_2}-dp_{j_1}}{tot_{j_2}-tot_{j_1}} < \frac{dp_{j_3}-dp_{j_2}}{tot_{j_3}-tot_{j_2}} totj2totj1dpj2dpj1<totj3totj2dpj3dpj2

即直线 j 1 → j 2 j_1 \to j_2 j1j2 k k k 小于 j 2 → j 3 j_2 \to j_3 j2j3 直线的 k k k ,本质上是这两条直线的斜率关系。


因此,我们需要维护 相邻两点间直线的 k k k (斜率) ,并当它们 单调递增 时, j 2 j_2 j2 所得到的 b b b 就可能是最小值。

那么什么时候 j 2 j_2 j2 所取的 b b b 就一定是最小值呢?

image.png

我们发现,当一段单调递增的 k k k 满足一个点的左边的 k ’ k’ k 都小于 k k k ,右边的 k ’ k’ k 都大于 k k k 时,这个点就是使 b b b 最小的点。

如果我们只维护 相邻两点间连线斜率大于等于 k k k k ′ k' k (斜率),那么在这个单调递增的序列中最小值就能使 b b b 最小。

这不就是单调队列的思路吗?

总结一下:

  1. 我们用单调队列维护相邻两点间直线的 k k k ,使其单调递增。
  2. 在单调队列里放的是 k k k 单调递增的点的编号。
  3. 最终答案是单调队列的队头坐标代入 d p i = d p j + s u m i ∗ t o t i + t o t n ∗ s − t o t j ∗ ( s u m i + s ) dp_i = dp_j + sum_i*tot_i + tot_n*s - tot_j*(sum_i+s) dpi=dpj+sumitoti+totnstotj(sumi+s).
  4. 为了维护单调性,我们需要从左侧队头开始删除。即判断队头斜率 d p q h e a d + 1 − d p q h e a d t o t q h e a d + 1 − t o t q h e a d ≤ s + s u m i \frac{dp_{q_{head+1}}-dp_{q_{head}}}{tot_{q_{head+1}}-tot_{q_{head}}} \leq s+sum_i totqhead+1totqheaddpqhead+1dpqheads+sumi 时,把队头出队即可。 为了避免精度问题,且 t o t tot tot 有单调递增性,那么我们不妨判断 d p q h e a d + 1 − d p q h e a d ≤ ( s + s u m i ) ∗ ( t o t q h e a d + 1 − t o t q h e a d ) {dp_{q_{head+1}}-dp_{q_{head}}} \leq (s+sum_i) * ({tot_{q_{head+1}}-tot_{q_{head}}}) dpqhead+1dpqhead(s+sumi)(totqhead+1totqhead).
  5. 添加时,如果 q i q_i qi 不能与前面的点满足单调性,那么直接把前面的点不断出队,直到满足单调性为止。即当 d p i − d p q t a i l t o t i − t o t q t a i l ≤ d p q t a i l − d p q t a i l − 1 t o t q t a i l − t o t q t a i l − 1 \frac{dp_{i}-dp_{q_{tail}}}{tot_{i}-tot_{q_{tail}}} \leq \frac{dp_{q_{tail}}-dp_{q_{tail-1}}}{tot_{q_{tail}}-tot_{q_{tail-1}}} totitotqtaildpidpqtailtotqtailtotqtail1dpqtaildpqtail1 时不断出队即可。同样避免精度问题,判断 ( d p i − d p q t a i l ) ∗ ( t o t q t a i l − t o t q t a i l − 1 ) ≤ ( d p q t a i l − d p q t a i l − 1 ) ∗ ( t o t i − t o t q t a i l ) ({dp_{i}-dp_{q_{tail}}}) * ({tot_{q_{tail}}-tot_{q_{tail-1}}}) \leq ({dp_{q_{tail}}-dp_{q_{tail-1}}})*({tot_{i}-tot_{q_{tail}}}) (dpidpqtail)(totqtailtotqtail1)(dpqtaildpqtail1)(totitotqtail) 即可。

时间复杂度 O ( n ) O(n) O(n) .

#include <bits/stdc++.h>
using namespace std;
const int N = 300005;
long long n,s,ans,t[N],c[N],dp[N],sum[N],tot[N];
long long q[N],head=1,tail=1;
int main()
{cin >> n >> s;for (int i=1;i<=n;i++){cin >> t[i] >> c[i];sum[i] = sum[i-1] + t[i];tot[i] = tot[i-1] + c[i];dp[i] = 1e18;}ans = 1e18;dp[0] = 0;for (int i=1;i<=n;i++){while (head < tail && dp[q[head+1]]-dp[q[head]] <= (s+sum[i])*(tot[q[head+1]]-tot[q[head]])) head++;dp[i] = dp[q[head]] + sum[i]*tot[i] + tot[n]*s - tot[q[head]]*(sum[i]+s);while (head < tail && (dp[i]-dp[q[tail]])*(tot[q[tail]]-tot[q[tail-1]]) <= (dp[q[tail]]-dp[q[tail-1]])*(tot[i]-tot[q[tail]])) tail--;q[++tail] = i;}cout<<dp[n];return 0;
}

为什么单调队列初始 head = 1,tail = 1 ,而不能写作 head = 1,tail = 0 ?
考虑到还有 dp[0] = 0 ,要么把 tail 设为 head ,要么把 tail 设为 head-1 再在队列里加入 dp[0]

后记

斜率优化看起来好像的确比想象中抽象很多,希望对大家理解斜率优化有所帮助!

http://www.xdnf.cn/news/611227.html

相关文章:

  • VS Code-i18n Ally国际化插件 配置百度翻译
  • 【北京盈达科技】GEO优化中的多模态了解
  • 基于 Spring Boot + Vue 的墙绘产品展示交易平台设计与实现【含源码+文档】
  • MySQL备份工具:XtraBackup
  • Vue3 + Element Plus 中修改表格当前选中行的颜色
  • Linux——网络基础概念
  • multipart/form-data
  • 光伏电站及时巡检:守护清洁能源的“生命线”
  • 图解深度学习 - 深度学习的工作原理
  • PostgreSQL中的权限管理简介
  • 【49. 字母异位词分组】
  • 各类Agent技术的发展现状和核心痛点
  • 【实测案例】碳纤维复合材料成型过程温度及应变变化监测
  • Docker部署OpenSearch集群
  • git初始化及操作指南
  • 4408. 李白打酒加强版(dp)
  • Redis Scan代替Keys优化
  • 2025国内领先GEO服务商上海源易:AI赋能下的GEO内容创新与实践
  • Linux iSCSI存储共享实验指南
  • NFS服务小实验
  • SkyWalking启动失败:OpenSearch分片数量达到上限的完美解决方案
  • c语言字符串函数
  • 深入浅出 Python Testcontainers:用容器优雅地编写集成测试
  • Java详解LeetCode 热题 100(20):LeetCode 48. 旋转图像(Rotate Image)详解
  • 皮尔森电流互感器在浪涌电流测试中的应用
  • 如果教材这样讲---开关电源的拓扑结构
  • 路由协议RIP配置与分析
  • 现代软件开发利器
  • C++成员对象和封闭类
  • 【鼎的3D设计与AI提示词方案】