斜率优化dp模型整理

300. 任务安排1(300. 任务安排1 - AcWing题库)

思路:很明显这些任务是按顺序排好的,我们能执行的操作只是对它们进行分批,我们可以发现每一批之前的开始时间s,影响的不仅仅是当前这一批的结束时间,而是将后面所有的结束时间都往后推了s,那么我们直接把对于后面的影响全部提到当前位置来算,那么批与批之间就相对独立一点了。

我们这么来定义,定义dp[i]表示第i个物品作为当前这批的末尾,那么我们就可以通过对上一批的末尾进行讨论进而划分批次,上一批的末尾可以从1一直到i-1。

令sc作为费用的前缀和,st作为时间的前缀和

那么状态转移就是:
dp[i]=min(dp[j]+(sc[i]-sc[j])*st[i]+s*(sc[n]-sc[j]));

//因为前面的s对于当前的影响已经被计算到之前批次中了,所以我们这里只用计算当前批次的影响即可。

那么显然时间复杂度是O(n^2),对于这题的数据范围来说是可以ac的。

另外因为这道题求的是最小值,所以说,我们初值要赋成正无穷。

#include<bits/stdc++.h>
using namespace std;
#define int long long
int t[6000],c[6000],dp[6000];
signed main()
{int n,s;scanf("%lld%lld",&n,&s);for(int i=1;i<=n;i++) {scanf("%lld%lld",&t[i],&c[i]);c[i]+=c[i-1];t[i]+=t[i-1];}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]+(c[i]-c[j])*t[i]+s*(c[n]-c[j]));}}cout<<dp[n];
}

301. 任务安排2(301. 任务安排2 - AcWing题库)

在题意上较之上题没有变化,但是n的数据范围变大了。

所以递推的式子还是如上: dp[i]=min(dp[j]+(sc[i]-sc[j])*st[i]+s*(sc[n]-sc[j]));

但显然不能再用暴力去循环了,那么换个思路。我们来观察下这个式子。很容易发现,与j有关的只有两类:dp[j],sc[j],如果我们将两者一个视为自变量,一个视为因变量,那么很显然是一个一次函数:

dp[j]=(st[i]+s)*sc[j]+dp[i]-sc[i]*st[i]-s*sc[n];

显然i确定后,st[i]+s是确定的,另外对于j来说,一个j,只有一个dp[j]和一个sc[j],那么实际上就相当于将所有的j的点画在坐标系中,然后一条斜率确定的,经过这些点的直线,求满足条件的直线的截距的最小值。

显然如果找截距最小的话,应该是图中红线上的点(相当于凸包的下边界),很容易发现当截距确定后,红色边界上面的点产生的截距小于红色边界上的点。那么我们该如何去找,我们确定i之后实际还需要sc[i]和dp[j]才能求出我们需要的值。如图中的绿线,它经过的点,显然是从下边界中找到两个斜率,一个大于它,一个小于它,由此确定出它经过哪个点。那么如果我们维护出一个存斜率的单调队列,就很容易查找目标位置,可以用二分来查找。

但是再观察一下式子,我们会发现(st[i]+s)和sc[j]都随着j的变大而变大,所以很明显,当前的斜率如果大于一部分斜率的话,那么后面的只会更大,所以前面的根本不会再被访问到,那么就没有存下来的意义,于是我们实际可以在O(1)的时间复杂度内完成查找。

这么来说,每个点可以通过的线有很多条,现在已知有哪些点了,需要找出一条斜率已知的直线通过哪个点的截距最小,显然可以通过维护已知点的下凸壳得到,另外,凸壳边的斜率不等价于将这个点放入时的直线斜率。所以尽管放入的点对应的直线的斜率越来越大,但是凸壳的斜率也会有大于和小于的。当然如图中的绿线,这个点被放入后,k2线后面的两个点便不会再作为结果中的点了,因为有更合适的点出现了。

所以我们查询和放入时的删除操作不同,查询的时候,是这么考虑的,如果k1没被用到,也即k1<k,那么后续放入的线的k只会更大,那么至少得从2开始找(因为当前这个小于k2),所以压根用不到k1,那么我们是将点1弹出。

在放入的时候,很显然当前的绿线一旦放入,点3和点4就不会再作为凸壳的边了,因为新放入的点显然与2之间连的边变成凸壳的下边界了。

所以我们查询的时候是按照凸壳的边来查询的,修改的时候修改的也是凸壳的边,所以我们实际上维护的是凸壳下边界。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll t[300010],c[300010],dp[300010],q[300010],hh,tt;
int main()
{int n,s;scanf("%d%d",&n,&s);for(int i=1;i<=n;i++){scanf("%lld%lld",&t[i],&c[i]);t[i]+=t[i-1];c[i]+=c[i-1];}hh=0,tt=1;//0是需要被放入的for(int i=1;i<=n;i++){while(hh<tt&&(dp[q[hh+1]]-dp[q[hh]])<(c[q[hh+1]]-c[q[hh]])*(t[i]+s) ) hh++;int j=q[hh];dp[i]=dp[j]-(t[i]+s)*c[j]+c[i]*t[i]+s*c[n];//这一步要保证队列中至少有两个元素,否则直接放入即可while(hh+1<tt&&(dp[q[tt-1]]-dp[q[tt-2]])*(c[i]-c[q[tt-2]])>=(dp[i]-dp[q[tt-2]])*(c[q[tt-1]]-c[q[tt-2]])) tt--;q[tt++]=i;}cout<<dp[n];
}

一定要明确的就是凸壳的斜率和点 被放入时对应的直线斜率不是一回事,凸壳的斜率只是用来辅助查找点的,与直线斜率没有直接关系,另外凸壳的斜率的维护也要注意,查询和插入对应的是不同的修改策略。

302. 任务安排3(302. 任务安排3 - AcWing题库)

思路:这题变化的地方在于t的范围,t可能为负值, 范围很大很明显没有办法暴力,那么还是来考虑优化:

dp[j]=(st[i]+s)*sc[j]+dp[i]-sc[i]*st[i]-s*sc[n];

这里直线的斜率由于t为负,那么就可能就不是单增了,所以在查询的时候是没办法修改的,因为当前的直线没用到凸壳的某个斜率,不代表后面的直线不会用到,但是凸壳仍然是维护一个单增的序列(这个应该是凸壳的性质。)直线斜率为负也没关系,仍然是找第一个大于直线斜率的凸壳斜率,所以不用担心,如下图:

所以相当于这里凸壳的维护不做更改,仅仅查询改变一下即可。也就是通过二分来查询符合要求的点,然后修改的维护同前。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll t[300010],c[300010],dp[300010],q[300010],hh,tt;
int main()
{int n,s;scanf("%d%d",&n,&s);for(int i=1;i<=n;i++){scanf("%lld%lld",&t[i],&c[i]);t[i]+=t[i-1];c[i]+=c[i-1];}hh=0,tt=1;//0是需要被放入的for(int i=1;i<=n;i++){int l=hh,r=tt-1;while(l<r){int mid=(l+r)/2;if((dp[q[mid+1]]-dp[q[mid]])>=(c[q[mid+1]]-c[q[mid]])*(t[i]+s) ) r=mid;else l=mid+1;}int j=q[l];dp[i]=dp[j]-(t[i]+s)*c[j]+c[i]*t[i]+s*c[n];//这一步要保证队列中至少有两个元素,否则直接放入即可while(hh+1<tt&&(double)(dp[q[tt-1]]-dp[q[tt-2]])*(c[i]-c[q[tt-2]])>=(double)(dp[i]-dp[q[tt-2]])*(c[q[tt-1]]-c[q[tt-2]])) tt--;q[tt++]=i;}cout<<dp[n];
}

 ps:这里相乘那一块儿有可能溢出long long,故而使用double 

摘自网上资料:
其实double可以存储比unsigned long long更大的数字

原因是无符号 long long 会存储精确整数,而 double 存储尾数有限的精度和一个指数。

这允许 double 存储非常大的数字(大约10^308 ),一个 double 中有大约15个(近16个)有效的十进制数字,其余的可能的小数是零(实际上是未定义的,但是你可以假定为”零”更好的理解)。

一个无符号long long 只有19个数字,但是每一个数字都被精确定义。

也就是说double的精度(15或16位)比long long(19位)位要小,但是由于double只表示十进制的15或16位有效数字和它的指数,所以负值取值范围为 -1.7976E+308 到 -4.94065645841246544E-324,正值取值范围为 4.94065645841246544E-324 到 1.797693E+308。

对于long long
其64位的范围应该是[-2^63 ,2^63],既-9223372036854775808~9223372036854775807。
它的存储方法就是按位存储。有符号位就有一位符号位,没有符号位就64位全部来存储这个数。

正是double不同于long long的存储方法,使得它虽然只有64位但是可以比同样是64位的long long 类型取值范围大很多。

 303. 运输小猫(303. 运输小猫 - AcWing题库)

题目大意:现在有m只猫,在n座山上玩,已知相邻两座山的间距,以及每只猫在哪座山上的游玩时间,一旦游玩结束,猫就变成等待状态,饲养员从1号山出发去接猫,只能接在等待状态的猫,其余的猫都不可以被接走。求猫的等待时间的最小和。

我们可以通过山的间距算出每座山到1的距离,又因为速度是1m/s,那么距离和时间相当于1:1的关系,设出发时间为s,那么到达第i座山的时间就是s+di,这座山上的某只猫的玩耍时间为ti的话,那么ti + wi=s+di,所以对每只猫来说,如果不用等待,那么显然s=ti-di,这即饲养员的最早出发时间,它的自身属性相当于就是ai=ti-di,另外注意到,饲养员的出发时间可以为负值,所以di>ti也无所谓。那么分析到这里,看似又没办法继续分析了。这里既然有这么多猫,我们按照ai排个序看看,因为我们排序的依据时每只猫恰好被接走时饲养员的最早出发时间,那么我们要一次接走一整段上的猫,肯定要以这段中的最晚时间为准,那么这段中猫的等待时间则为(ai-a1)+(ai-a2)+...+(ai-ai),所以我们可以将猫分段,那么这个问题就与我们之前的问题有些关联了。

定义dp[i][j]表示第i只猫恰好被第j个饲养员收走,我们可以通过找上一段猫的结尾位置来更新状态:

dp[i][j]=dp[k][j-1]+ai*(i-k+1)-(a[k+1]+a[k+2]+...+a[i])

令s[i]=a[1]+a[2]+...+a[i]:

则:dp[i][j]=dp[k][j-1]+ai(i-k+1)-(s[i]-s[k]);

移项可得:

dp[i][j]-ai*(i-k+1)+s[i]=dp[k][j-1]+s[k]

dp[k][j-1]+s[k]=a[i]*k+dp[i][j]-a[i]*(i+1)+s[i];

那么就跟上题关联起来了,而且这里的a[i]单增,我们甚至都不需要用二分来查找。

哦对了,另外,为了优化程序我们改一下两个维度,定义dp[j][i],因为我们循环的第一维是j的那一维,这样定义的话,磁盘读取会快一些。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=100010,M=100010,P=110;ll t[N],d[N],a[N],s[N];
ll dp[P][M];
int q[M],hh,tt;
ll get(int i,int j)
{return dp[j-1][i]+s[i];
}
int main()
{int n,m,p;scanf("%d%d%d",&n,&m,&p);for(int i=2;i<=n;i++){scanf("%lld",&d[i]);d[i] += d[i-1];}for(int i=1;i<=m;i++){int h;scanf("%d%lld",&h,&t[i]);a[i]=t[i]-d[h];}sort(a+1,a+1+m);for(int i=1;i<=m;i++) s[i]=s[i-1]+a[i];memset(dp,0x3f,sizeof dp);for(int i=0;i<=p;i++) dp[i][0]=0; for(int j=1;j<=p;j++){//dp[k][j-1]+s[k]=a[i]*k+dp[i][j]-a[i]*(i+1)+s[i];hh=0,tt=1;q[0]=0;for(int i=1;i<=m;i++){while(hh+1<tt&&(get(q[hh+1],j)-get(q[hh],j))<=a[i]*(q[hh+1]-q[hh])) hh++;int k=q[hh];dp[j][i]=dp[j-1][k]+a[i]*(i-k)-(s[i]-s[k]);while(hh+1<tt&&(get(q[tt-1],j)-get(q[tt-2],j))*(i-q[tt-1])>=(get(i,j)-get(q[tt-1],j))*(q[tt-1]-q[tt-2])) tt--;//我们维护的是上一层,所以这里是get(i,j)q[tt++]=i;}}cout<<dp[p][m];
}

ps:这里查询的时候就要严格保证队列中至少有两个元素再进循环,因为队列重复使用,没清。

斜率优化dp主要有两种类型,都是首先将状态转移的式子变形成一个一元函数,然后通过斜率与截距的关系来优化。

优化涉及到两种类型,一种是斜率和自变量都单增,那么维护单调队列即可实现查找,一种是仅自变量单增,那么就需要用二分来查找,另外凸包下边界的维护对于两者来说是一样的。 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/655121.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

8.6跳跃游戏②(LC45-M)

算法&#xff1a; 与上一题一样&#xff0c;还是看最大覆盖范围 要从覆盖范围出发&#xff0c;不管怎么跳&#xff0c;覆盖范围内一定是可以跳到的&#xff0c;以最小的步数增加覆盖范围&#xff0c;覆盖范围一旦覆盖了终点&#xff0c;得到的就是最少步数&#xff01; 这里…

C++大学教程(第九版)7.30 打印array对象 7.31 逆序打印字符串(递归练习题)

文章目录 题目代码运行截图题目代码运行截图 题目 (打印array对象)编写一个递归函数printArray它以一个array对象一个开始下标和一个结束下标作为实参&#xff0c;不返回任何值并打印这个array对象。当开始下标和结束下标相等时&#xff0c;这个函数应该停止处理并返回。 代码…

跨镜网络解决方案:SD-WAN专线,实现企业全球互联

在全球化的背景下&#xff0c;越来越多的企业需要在海外社媒平台推广、研发访问海外平台、实现海外分部和国内互联互通等需求。然而&#xff0c;面对不同地区之间网络连接不稳定、高延迟、高成本等问题&#xff0c;如何实现企业的全球互联成为了一个亟待解决的难题。 幸运的是&…

laravel学习笔记

这两天公司活不多&#xff0c;学习了一下laravel框架。经过几天的学习&#xff0c;也对它有一些大概的了解。现在我就把我所学的到在这儿做下笔记吧。 一、laravel集合 其实&#xff0c;这里所说的集合&#xff0c;跟前端es6中的集合概念还是有那一点的不太一样。前端es6的集合…

C++ 数论相关题目:卡特兰数应用、快速幂求组合数。满足条件的01序列

给定 n 个 0 和 n 个 1 &#xff0c;它们将按照某种顺序排成长度为 2n 的序列&#xff0c;求它们能排列成的所有序列中&#xff0c;能够满足任意前缀序列中 0 的个数都不少于 1 的个数的序列有多少个。 输出的答案对 1097 取模。 输入格式 共一行&#xff0c;包含整数 n 。 …

Java 集合 02 综合练习+基本数据类型对应的包装类

练习1、 自己写的代码&#xff1a; import java.util.ArrayList; public class practice {public static void main(String[] args) {//定义一个集合ArrayList<String> list new ArrayList<>();list.add("aaa");list.add("bbb");list.add(…

python实现豆瓣电影搜索并自动添加相关信息

1.豆瓣电影搜索 2.豆瓣电影信息自动添加 搜索 众所周知&#xff0c;豆瓣搜索有加密&#xff0c;得解密才行&#xff0c;还好网上众多大神都给破解了&#xff0c;那咱们拿来直接使用就行 相关代码仓库&#xff1a;xadmin-server/movies/utils/douban/search.py at movies nin…

React中使用LazyBuilder实现页面懒加载方法一

前言&#xff1a; 在一个表格中&#xff0c;需要展示100条数据&#xff0c;当每条数据里面需要承载的内容很多&#xff0c;需要渲染的元素也很多的时候&#xff0c;容易造成页面加载的速度很慢&#xff0c;不能给用户提供很好的体验时&#xff0c;懒加载是优化页面加载速度的方…

AJAX进阶(重点)

目录 ◆ 同步代码和异步代码 ◆ 回调函数地狱和 Promise 链式调用 什么是回调函数地狱&#xff1f; Promise - 链式调用 什么是Promise链式调用&#xff1f; Promise 链式应用 &#xff08;重点&#xff09; ◆ async 和 await 使用 async函数和await_捕获错误 ◆ 事…

二分查找|详细讲解|两种写法

二分查找 目录 1 介绍2 例题引入3 “左闭右闭”写法4 “左闭右开”写法 1 介绍 二分查找适用于从一个递增或递减的有序数列中查找某一个值 因此&#xff0c;使用二分查找的条件是&#xff1a; 用于查找的内容从逻辑上来看是有序的查找的数量只能是一个而不是多个 在二分查…

RAG应用-七个最常见的故障点

近日&#xff0c;国外研究者发布了一篇论文《Seven Failure Points When Engineering a Retrieval Augmented Generation System》&#xff0c;探讨了在实际工程落地RAG应用过程中容易出的七类问题。 论文地址&#xff1a;https://arxiv.org/pdf/2401.05856.pdf 一、丢失内容&…

【Go-zero】手把手带你在goland中创建api文件并设置高亮

【Go-zero】手把手带你在goland中创建api文件并设置高亮 大家好 我是寸铁&#x1f44a; 总结了一篇手把手带你在goland中创建api文件并设置高亮解决方案的文章✨ 喜欢的小伙伴可以点点关注 &#x1f49d; 问题复盘 在使用go-zero 框架时&#xff0c;常常需要用到goctl 一键生成…

简单易用的购物车

实现了购物基本功能&#xff0c;那来修改一下就可以用&#xff0c;app,h5,小程序都可以 购物车插件 - DCloud 插件市场

会话技术复习笔记

一.登录校验的需求 什么是登录校验&#xff1f; 所谓登录校验&#xff0c;指的是我们在服务器端接收到浏览器发送过来的请求之后&#xff0c;首先我们要对请求进行校验。先要校验一下用户登录了没有&#xff0c;如果用户已经登录了&#xff0c;就直接执行对应的业务操作就可以…

数据结构——用Java实现二分搜索树

目录 一、树 二、二分搜索树 1.二叉树 2.二分搜索树 三、代码实现 1.树的构建 2.获取树中结点的个数 3.添加元素 4.查找元素 &#xff08;1&#xff09;查找元素是否存在 &#xff08;2&#xff09;查找最小元素 &#xff08;3&#xff09;查找最大元素 5.二分搜索…

合并有序链表---链表OJ---归并思想

https://leetcode.cn/problems/merge-two-sorted-lists/?envTypestudy-plan-v2&envIdtop-100-liked 将两个有序的链表合并为一个新的有序链表&#xff0c;那不就是和归并排序中最后合并的思想一样吗&#xff1f;只不过那里合并的是数组&#xff0c;这里合并的是链表。 首先…

StartAI宝藏功能——关键词解析

AIGC的同学们都有一个“世纪难题”——生图咒语。 想要生成高质量的图&#xff0c;关键词是至关重要地。关键词往往能决定一张图的成败&#xff01;当你看见一张戳中你审美的图&#xff0c;想模仿却不知如何下手&#xff0c;只能全网搜索生图咒语。 费事又费力&#xff0c;甚…

leetcode刷题(剑指offer) 50.Pow(x, n)

50.Pow(x, n) 实现 pow(x, n) &#xff0c;即计算 x 的整数 n 次幂函数&#xff08;即&#xff0c;xn &#xff09;。 示例 1&#xff1a; 输入&#xff1a;x 2.00000, n 10 输出&#xff1a;1024.00000示例 2&#xff1a; 输入&#xff1a;x 2.10000, n 3 输出&#x…

微信小程序|摇骰子

目录 简介设计与功能需求确定用户界面设计确定摇骰子动画效果确定随机数生成算法编码实现实现摇骰子动画测试与优化进行功能测试进行性能测试说明简介 制作一个摇骰子小程序是一个有趣且具有挑战性的项目。通过这个项目,你可以学习如何运用编程技术来模拟骰子的摇动和结果显示…

【LeetCode每日一题】56. 合并区间插入区间

一、判断区间是否重叠 力扣 252. 会议室 给定一个会议时间安排的数组 intervals &#xff0c;每个会议时间都会包括开始和结束的时间 intervals[i] [starti, endi] &#xff0c;请你判断一个人是否能够参加这里面的全部会议。 思路分析 因为一个人在同一时刻只能参加一个会…