斜率优化dp模型整理

news2024/11/19 10:40:53

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.coloradmin.cn/o/1419041.html

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

相关文章

Java强训day9(选择题编程题)

选择题 class Person {String name "No name";public Person(String nm) {name nm;} } class Employee extends Person {String empID "0000";public Employee(String id) {super(" ");//要调用父类的有参构造方法否则报错empID id;} } pu…

常用芯片学习——AMS1117芯片

AMS1117 1A 低压差线性稳压器 使用说明 AMS1117 是一款低压差线性稳压电路&#xff0c;该电路输出电流能力为1A。该系列电路包含固定输出电压版本和可调输出电压版本&#xff0c;其输出电压精度为士1.5%。为了保证芯片和电源系统的稳定性&#xff0c;XBLWAMS1117 内置热保护和…

秋招面试—浏览器原理篇

浏览器原理篇 1.什么是XSS、CSRF,怎么预防&#xff1f; &#xff08;1&#xff09;XSS(跨站脚本攻击)&#xff1a;攻击者将恶意代码植入到浏览器页面中&#xff0c;盗取存储在客户端的Cookie&#xff1b; ​ XSS分为&#xff1a;①存储型&#xff1a;论坛发帖、商品评论、用户…

大型电商系统商城源码_架构_订单系统_OctShop

中国的电商差不多发展到今天已经有20多年的历史啦&#xff0c;特别是过去的10年里其发展速度与竞争是相当的激烈&#xff0c;发展出了各种各样的模式如&#xff1a;B2B、B2C、B2B2C、O2O、社交电商等等。对于广大的企业或商家来说&#xff0c;电商是一个不可或缺的销售渠道&…

猫咪不吃东西怎么办?公认适口性好的生骨肉冻干分享

猫咪不吃东西怎么办&#xff1f;遇到这类情况主人需要仔细观察并分析情况。如果猫咪出现其他异常症状&#xff0c;如呕吐、腹泻、体温异常等&#xff0c;可能是生病了&#xff0c;应及时就医。如果猫咪没有其他异常症状&#xff0c;那猫咪不吃东西怎么办&#xff1f;可能是猫粮…

JAVA多线程并发补充

AQS 是一个抽象父类 全称是 AbstractQueuedSynchronizer&#xff0c;是阻塞式锁和相关的同步器工具的框架。 用 state 属性来表示资源的状态&#xff08;分独占模式和共享模式&#xff09;&#xff0c;子类需要定义如何维护这个状态&#xff0c;控制如何获取锁和释放锁 getSt…

Windows系统云服务器自定义域名解析导致网站无法访问怎么解决?

本文九河云介绍Windows实例内部自定义域名解析与本地网络域名解析不一致导致无法访问网站的问题描述、问题原因和解决方案。 问题描述 在Windows实例内部通过浏览器无法访问某网站&#xff0c;但在其他设备上可以正常访问&#xff0c;排查发现Windows实例内部自定义域名解析与…

6.s081 学习实验记录(三)system calls

文章目录 一、use gdb二、syscall&#xff1a;trace注意&#xff1a;实验代码&#xff1a;实验结果&#xff1a; 三、sysinfotips&#xff1a;实验代码实验结果 需要切换到 syscall 分支 一、use gdb 学习使用 gdb 调试 make qemu-gdb打开一个新的终端&#xff1a; gdb-mult…

Selenium教程11:模拟账号密码,自动登入qq空间

Python爬虫教程30&#xff1a;Selenium网页元素&#xff0c;定位的8种方法&#xff01; Selenium自动化教程02&#xff1a;浏览器options配置及常用的操作方法 Selenium自动化教程03&#xff1a;延时等待的3种方式 Selenium自动化教程04&#xff1a;鼠标键盘网页的模拟操作 …

文件的相关概念及用法

文件的作用 程序运行时产生的数据都属于临时文件&#xff0c;程序一旦运行结束就会被释放。若想让数据保存下来&#xff0c;则可以通过文件将数据持久化 文件需要包含的头文件<fstream>(文件流) 文件分类 文件分为文本文件和二进制文件。 文本文件&#xff1a;文件以…

快速上手Git

目录 一、Git概述 二、Git的常用命令 Git全局配置 获取Git仓库 基本概念 本地仓库操作 远程仓库操作 分支操作 标签操作 三、在IDEA中使用Git 在IDEA中配置Git 本地仓库操作 远程仓库操作 分支操作 冲突解决 一、Git概述 Git是一个分布式版本控制工具&…

探索设计模式的魅力:深入了解适配器模式-优雅地解决接口不匹配问题

设计模式专栏&#xff1a;http://t.csdnimg.cn/nolNS 目录 一、引言 1. 概述 2. 为什么需要适配器模式 3. 本文的目的和结构 二、简价 1. 适配器模式的定义和特点 定义 特点 2. 适配器模式的作用和适用场景 作用 适用场景 3. 适配器模式与其他设计模式的比较 三、适配…

DolphinScheduler + Amazon EMR Serverless 的集成实践

01 背景 Apache DolphinScheduler 是一个分布式的可视化 DAG 工作流任务调度开源系统&#xff0c;具有简单易用、高可靠、高扩展性、⽀持丰富的使用场景、提供多租户模式等特性。适用于企业级场景&#xff0c;提供了一个可视化操作任务、工作流和全生命周期数据处理过程的解决方…

代码随想录算法刷题训练营day18

代码随想录算法刷题训练营day18&#xff1a;LeetCode(257)二叉树的所有路径、LeetCode(404)左叶子之和 LeetCode(257)二叉树的所有路径 题目 代码 import java.util.ArrayList; import java.util.List;/*** Definition for a binary tree node.* public class TreeNode {* …

媒体邀约:怎么吸引总体目标受众?

新闻媒体影响力日益扩大。不论是公司、机构还是其他&#xff0c;都希望能够通过新闻媒体的曝光来吸引更多总体目标受众。要想真正吸引住总体目标受众并非易事&#xff0c;需要一定的方案和方法。下面我们就深入探究媒体邀约推广的真相&#xff0c;共享怎么吸引总体目标受众的方…

挂耳耳机哪个牌子好?挂耳耳机产品的几大推荐

如果你也是个运动爱好者&#xff0c;那你一定知道边运动边听歌是多么的提神! 我试过好多种耳机&#xff0c;那些长长的线总在运动时不断甩来甩去&#xff0c;真是让人烦不胜烦。而且戴久了耳朵里也不舒服。所以找一款真正适合运动的挂耳耳机太重要了&#xff0c;它能让你的运动…

【Docker】docker Overlay2 文件系统原理

概述 overlayFS是被称为联合文件系统的其中一个解决方案。在2014年&#xff0c;发布了第一个版本并且合并到了Linux的内核3.18版本中&#xff0c;此时&#xff0c;在docker被称为是overlay文件驱动。后来在Linux 内核4.0 版本中进行了改进&#xff0c;称为overlay2。&#xff…

模拟堆

import java.util.Scanner;public class Main{static int N 100010, size,m;static int[] h new int[N]; //h[i]表示下标i这个点是第多少static int[] hp new int[N]; //hp[i]表示下标为i的节点是第几个被加进来的static int[] ph new int[N]; //ph[i]表示第i个加进来…

BIO、NIO变成与直接内存、零拷贝

一、网络通信 1、什么是socket&#xff1f; Socket 是应用层与 TCP/IP 协议族通信的中间软件抽象层&#xff0c;它是一组接口&#xff0c;一般由操作 系统提供。客户端连接上一个服务端&#xff0c;就会在客户端中产生一个 socket 接口实例&#xff0c;服务端每接受 一个客户端…