基础dp——动态规划

news2025/2/24 7:13:25

目录

一、什么是动态规划?

二、动态规划的使用步骤

1.状态表示

2.状态转移方程

3.初始化

4.填表顺序

5.返回值

三、试题讲解

1.最小花费爬楼梯

2.下降路径最小和

3.解码方法


一、什么是动态规划?

        动态规划(Dynamic Programming,简称dp)是一种通过将复杂问题分解为更小的子问题来解决的算法思想,尤其适用于具有重叠子问题最优子结构的优化问题。其核心目标是避免重复计算,通过存储中间结果(记忆化)来提升效率。

动态规划 vs 分治法

  • 共同点:都将问题分解为子问题。

  • 区别

    • 分治法(如归并排序)的子问题独立,无重叠,无需存储中间结果。

    • 动态规划的子问题有重叠,需存储中间结果避免重复计算。

二、动态规划的使用步骤

        为了方便理解下面我将从感性的角度来给大家讲解动态规划的使用步骤

1.状态表示

        首先在解决问题的时候我们通常会使用一块空间来记录已处理的数据,我们把这块空间叫作dp表。

        当我们在解决一个小问题时需要借助之前已解决的问题的数据,就可以到dp表里面查,当前这个小问题解决后继续把它相关的信息存到dp表,然后继续解决下一个小问题。

        这个dp表中的一小块数据表示什么?这些问题指的就是状态表示。在用动态规划解决问题时,要面对最重要的问题就是dp表的状态应该表示是什么?

在解决这个问题时我们通常都是从这几个角度去思考:

  • 1.经验+题目要求
  • 2.分析问题的过程中,发现重复子问题。
  • 3.当我们发现子问题后,假设前(或后)几个小问题已经解决,思考我们在解决当前问题时有没有需要前面已解决的问题的什么信息?

2.状态转移方程

        dp[i]等于什么?这就是状态转移方程。它通常要依赖于已经计算过的状态。

3.初始化

在创建好dp表后主要任务就是对dp表的填写,初始化dp表的作用有以下两点:

  1. 保证填表的时候不越界。
  2. 保证填表的正确性。

4.填表顺序

        为使填写当前状态的时候,所需要的状态已经计算过了。

5.返回值

        最后结合题目要求和状态表示计算结果并返回。

三、试题讲解

1.最小花费爬楼梯

        首先我们分析题目,我们的起点是0或1的位置,而终点是n位置(注意不是n-1位置)。我们在爬楼梯时支付一次费用可以爬一个或两个台阶。

        当我们尝试从动态规划方向去解决,那么我们试着去构造一下相同的子问题,并且假设它已经解决。

注:为了方便在下文我都会把动态规划简称为“动规” 

        用动规解题过程中,在找子问题时我总结了一个很厉害的小技巧,就是保留主问题的逻辑,而换一个问题的对象。比如这里,主问题是起点爬到楼顶的最小花费,那么构造子问题时我们只需要保留这个问题的逻辑,而把楼顶这个对象改成任意位置i。那么我们就得到了一个子问题(即从起点爬到i位置的最小花费)。然后我们再假设前面的相同的子问题已经解决。

比如:

        动规题复杂多变,以上技巧可以解决大部分的基础动规题,但更多的只是作为一个小经验,并不是所有动规题都可以通过构建子问题来解决的。

        在解一个题时,状态表示可以是很多,不同的状态表示就决定了不同的状态转移方程,而要判断一个状态表示是否正确只需看是否能根据该状态表示推出正确的状态转移方程。

能够理解上图那么动态规划的五步骤就水到渠成了。

  • 状态表示: dp[i]:表示爬到i时的最小花费
  • 状态转移方程: dp[i]=min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2])
  • 初始化: dp表初始化为全0
  • 填表顺序: 从下标为2的位置开始,并且从左往右填写
  • 返回值: return dp[n]

代码示例:

class Solution {
public:
    int minCostClimbingStairs(vector<int>& cost){
        int n=cost.size();
        vector<int> dp(n+1);//默认值就是0,不用初始化
        for(int i=2;i<=n;i++) 
            dp[i]=min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2]);
        return dp[n];    
    }
};

        除了刚才的状态表示我们还可以换一个,只需要改变一下子问题,同样的技巧,主问题逻辑不变,把起点这个对象改成任意位置i, 那么我们就得到了一个子问题(即从i位置爬到楼顶的最小花费)。

  • 状态表示: dp[i]:表示从i爬到楼顶的最小花费
  • 状态转移方程: dp[i]=cost[i]+min(dp[i+1],dp[i+2])
  • 初始化: dp[n-1]=cost[n-1],dp[n-2]=cost[n-2]
  • 填表顺序: 从右往左
  • 返回值: return min(dp[0], dp[1])

        我们可以发现状态表示的改变使得其他四个步骤的实现逻辑改变,所以可见状态表示的重要性。

代码示例:

class Solution {
public:
    int minCostClimbingStairs(vector<int>& cost){
        int n=cost.size();
        vector<int> dp(n);
        dp[n-1]=cost[n-1],dp[n-2]=cost[n-2];
        for(int i=n-3;i>=0;i--) 
            dp[i]=cost[i]+min(dp[i+1],dp[i+2]);
        return min(dp[0],dp[1]);    
    }
};

        提示:空间优化:在我们填写某一个状态的时候只需要使用它的前两个状态,所以可以把dp数组简化为两个变量就可以解决问题(需要注意变量的更新)。通常把该优化方法叫做滚动数组”。很多基础动规都可以进行这样的优化,大家可以试着去实现,这一点在后面也不再提。 

        提示:为了减少逻辑上的赘述,在以下题解中我都只会讲解一种状态表示作为示例。 

2.下降路径最小和

通过上一题找子问题的经验,我们可以这样做:

抓住主问题:从第一行任意位置下降到最后一行的任意一个位置的最小和。 

        保留问题的主逻辑,把对象“第一行任意位置”或“最后一行任意位置”更改,比如更改“最后一行任意位置”为“第i行的任意位置j”。得到子问题,即从“第一行任意位置下降到第i行的任意位置j的最小和”

        接下来假设与它相同的子问题都解决,并利用它们的结果。如下:

动规五步骤:

  • 状态表示: dp[i][j]:从第一行任意位置下降到第i行j列的最小和
  • 状态转移方程: dp[i][j]=matrix[i][j]+min(dp[i-1][j-1],min(dp[i-1][j],dp[i-1][j+1]) )
  • 初始化:越界区域如下:

        

        创建(n+1)*(n+2)的dp表初始化为全INT_MAX(这样创建dp表减少了为防止越界而做特殊判断带来的成本),再将第一行初始化为全0把dp表初始化为全INT_MAX,是为了防止越界区域参与最小值的筛选再把第一行初始化为全0是逻辑需要,总的目的都是为了保证填表的正确性。

  • 填表顺序: 从上到下,从左往右(或从右往左,只要能保证在填写当前状态时需要的状态已计算过即可)
  • 返回值: 返回最后一行的最小值

代码示例: 

class Solution {
public:
    int minFallingPathSum(vector<vector<int>>& matrix)
    {
        int n=matrix.size();//题目明确指出是方形,所以行数和列数都一样。
        vector<vector<int>> dp(n+1,vector<int>(n+2,INT_MAX));
        for(int j=0;j<n+2;j++) dp[0][j]=0;
        for(int i=1;i<=n;i++)
            for(int j=1;j<=n;j++)
                dp[i][j]=matrix[i-1][j-1]+min(min(dp[i-1][j-1],dp[i-1][j]),dp[i-1][j+1]);
        int ret=INT_MAX;
        for(int j=1;j<=n;j++) ret=min(ret,dp[n][j]);
        return ret;
    }
};

3.解码方法

        主问题: 一个非空字符s的解码方法的总数。但我们好像无法从中获取到可以更改的对象,再回去观察上面我们解决的两道问题。它们主问题的逻辑中都是带有区间式的,比如第一题:从起点到终点,第二题:从第一行到最后一行。那么如果我们还想要那个找子问题的技巧去确定状态表示,那么在总结主问题时就要朝着带有区间式的方向去想。

        这里我们可以把主问题总结为:从s字符串的下标0开始到下标n-1结束的解码方法总数。那么这样我们就构造出了一个区间“开始”——“结束”。子问题就好构建得多了。

子问题:从下标0开始到下标i结束的解码方法总数。

  • 状态表示: dp[i]:表示从下标0开始到下标i结束的解码方法总数
  • 状态转移方程:

        解决了状态表示但这个题的难点主要在于状态转移方程。

     

         我们可以把单独解码和前一个联合解码分开讨论。如果单独解码成功相当于在i-1的所有解码方法中统一加上一个s[i],所以为dp[i-1],解码失败的话,说明不能单独解码,所以结果为0

        同理联合解码成功相当于在i-2的所有解码方法中统一加上一个s[i]与s[i-1]的复合,所以为dp[i-2],解码失败的话,说明不能联合解码所以结果为0最后只需要把单独解码与联合解码的结果相加。

  • 初始化: 因为我们在用dp[i]时需要用到dp[i-1]和dp[i-2]的状态,所以最好把dp[0]和dp[1]初始化。dp[0]可以这样写dp[0]=s[i]!='0',但dp[1]的初始化会比较麻烦,它的初始化逻辑和上面的状态转移方程是一样的。而在下面填表时同样要用到这样的逻辑,就会显得很累赘。

        我们可以使用这样一个初始化技巧:

        那么可以做一个n+1大小的dp表,然后错开一位表示,即 dp[i]:表示从下标0开始到下标i-1结束的解码方法总数。为保证后面的填表顺序正确我们需要dp[1]=s[i]!='0',这是必然的。而dp[0]该初始化为什么?我们可以思考什么时候用到dp[0],即s[0]与s[1]解码成功时,那么它必然是dp[0]=1

  • 填表顺序:从dp的2下标开始,并从左往右。
  • 返回值: return dp[n]

代码示例:

class Solution {
public:
    int numDecodings(string s)
    {
        vector<int> dp(s.size()+1,0);
        dp[1]=s[0]!='0',dp[0]=1;
        for(int i=2;i<dp.size();i++)
        {
            int m=(s[i-2]-'0')*10+s[i-1]-'0';
            if(s[i-1]!='0') dp[i]+=dp[i-1];
            if(m>=10&&m<=26) dp[i]+=dp[i-2];
        }
        return dp.back();
    }
};

非常感谢您能耐心读完这篇文章。倘若您从中有所收获,还望多多支持呀!74c0781738354c71be3d62e05688fecc.png

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

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

相关文章

(四)趣学设计模式 之 原型模式!

目录 一、 啥是原型模式&#xff1f;二、 为什么要用原型模式&#xff1f;三、 原型模式怎么实现&#xff1f;四、 原型模式的应用场景五、 原型模式的优点和缺点六、 总结 &#x1f31f;我的其他文章也讲解的比较有趣&#x1f601;&#xff0c;如果喜欢博主的讲解方式&#xf…

会话对象 Cookie 四、Cookie的路径

1.Cookie的path属性 Cookie还有一个path属性&#xff0c;可以通过Cookie#setPath(String)方法来设置。你可以使用HttpWatch查看响应中的Set-Cookie中是否存在路径。下面是通过Chrome查看Cookie信息。 也就是说&#xff0c;就算你不设置Cookie的path&#xff0c;Cookie也是有路…

hugging face---transformers包

一、前言 不同于计算机视觉的百花齐放&#xff0c;不同网络适用不同情况&#xff0c;NLP则由Transformer一统天下。transformer是2017年提出的一种基于自注意力机制的神经网络架构&#xff0c;transformers库是hugging face社区创造的一个py库&#xff0c;通过该库可以实现统一…

将 Vue 项目打包后部署到 Spring Boot 项目中的全面指南

将 Vue 项目打包后部署到 Spring Boot 项目中的全面指南 在现代 Web 开发中&#xff0c;前后端分离架构已经成为主流。然而&#xff0c;在某些场景下&#xff0c;我们可能需要将前端项目&#xff08;如 Vue&#xff09;与后端项目&#xff08;如 Spring Boot&#xff09;集成部…

GPIO外设

一、GPIO简介 GPIO&#xff0c;general-purpos IO port,通用输入输出引脚&#xff0c;所有的GPIO引脚都有基本的输入输出功能。 最基本的输出功能&#xff1a;STM32控制引脚输出高、低电平&#xff0c;实现开关控制&#xff1b;最基本的输入功能&#xff1a;检测外部输入电平&…

C++——priority_queue模拟实现

目录 前言 一、优先级队列介绍 二、优先级队列实现 向上调整 向下调整 三、仿函数 总结 前言 上一篇文章我们讲了stack和queue&#xff0c;这两个容器是容器适配器&#xff0c;本质上是一种复用&#xff0c;那本篇文章要讲的优先级队列也是一个容器适配器&#xff0c;我们…

计算机网络基础:DOS命令、批处理脚本常见命令

目录 1. DOS 基础命令 1. echo 、 > 编写文件 2. type 读取文件 3. copy con 整段编写 4. attrib 命令 5. 快速生成空文件 6. 修改关联性 7. 关机shutdown 8. 复制文件copy、移动文件move 9. 重命名ren 2. 批处理 2.1 简单显示 2.2 死循环 2.3 定时关机小程序 …

ArcGIS Pro热力图制作指南:从基础到进阶

引言 在地理信息科学领域&#xff0c;热力图作为一种直观的数据可视化手段&#xff0c;广泛应用于展示空间数据的密度和热度分布。ArcGIS Pro&#xff0c;作为一款强大的地理信息系统&#xff08;GIS&#xff09;软件&#xff0c;为我们提供了制作热力图的便捷工具。本文将从基…

智慧校园系统在学生学习与生活中的应用

随着科技的快速发展&#xff0c;智慧校园系统逐渐成为现代教育不可或缺的一部分。它整合了先进的信息技术、物联网技术以及人工智能等&#xff0c;旨在构建一个全面、智能、个性化的学习与生活环境。对于学生而言&#xff0c;这一系统不仅能够极大地提高学习效率&#xff0c;还…

第三十四周学习周报

目录 摘要Abstract1 文献阅读1.1 相关知识1.1.1 贝叶斯优化1.1.2 注意力机制复习 1.2 模型框架1.3 实验分析 总结 摘要 在本周阅读的文献中&#xff0c;作者提出了一种将注意力机制与LSTM相结合的模型AT-LSTM。虽然传统LSTM通过其门控机制能有效捕捉时间序列中的长期依赖关系&…

第4章 信息系统架构(三)

4.3 应用架构 应用架构的主要内容是规划出目标应用分层分域架构&#xff0c;根据业务架构规划目标应用域、应用组和目标应用组件&#xff0c;形成目标应用架构逻辑视图和系统视图。从功能视角出发&#xff0c;阐述应用组件各自及应用架构整体上&#xff0c;如何实现组织的高阶…

一周学会Flask3 Python Web开发-flask3模块化blueprint配置

锋哥原创的Flask3 Python Web开发 Flask3视频教程&#xff1a; 2025版 Flask3 Python web开发 视频教程(无废话版) 玩命更新中~_哔哩哔哩_bilibili 我们在项目开发的时候&#xff0c;多多少少会划分几个或者几十个业务模块&#xff0c;如果把这些模块的视图方法都写在app.py…

Android开发-深入解析Android中的AIDL及其应用场景

深入解析 Android 中的 AIDL 及其应用场景 1. 前言2. AIDL 的核心概念3. AIDL 的实现步骤3.1. 定义 AIDL 接口文件3.2. 实现服务端&#xff08;Service&#xff09;3.3. 客户端绑定与调用 4. AIDL 的典型应用场景4.1. 多进程应用4.2. 与系统服务交互4.3. 高性能 IPC4.4. 跨应用…

基于python的旅客游记和轨迹分析可视化系统设计(新)

项目地址&#xff1a;基于python旅客游记和轨迹分析可视化系统设计&#xff08;新&#xff09; 摘 要 旅客游记和轨迹分析可视化系统是一种能自动从网络上收集信息的工具&#xff0c;可根据用户的需求定向采集特定数据信息的工具&#xff0c;本项目通过研究爬取微博网来实现旅…

HackTheBox靶场之Unrested 漏洞CVE-2024-42327 《CUser 类中的 user.get 函数中的 SQL 注入》

目录 信息收集 web指纹收集 wappazer Nmap指纹收集 Nmap分析总结 漏洞利用 漏洞CVE-POC执行流程 信息收集 web指纹收集 wappazer 看着有apache2.4.52 那么可以试着找一下 apache的历史cve看可以利用否 使用用户名密码&#xff1a;matthew / 96qzn0h2e1k3 登录成后后…

uniprot系列相关数据库介绍

https://www.uniprot.org/uniprotkb/P49711/entry#family_and_domains 上面是一个CTCF human蛋白质条目&#xff0c; 我们来看看family & domain条目中涉及到的蛋白质家族以及结构域数据库&#xff1a; 1&#xff0c;funfam&#xff1a; CATH: Protein Structure Classi…

基于AIGC的图表自动化生成工具「图表狐」深度评测:如何用自然语言30秒搞定专业级数据可视化?

一、工具核心定位&#xff1a;自然语言驱动的数据可视化 作为数据科学从业者&#xff0c;我们常面临非技术同事的图表制作需求。传统流程需经历数据清洗→结构转换→图表配置→样式调整四大阶段&#xff0c;耗时且易出错。 图表狐&#xff08;官网预览&#x1f447;&#xff…

rpc到自己java实现rpc调用再到rpc框架设计

目录 rpc(Remote Procedure Call)rpc一般架构为什么要引入rpc自己实现rpc调用1. 新建一个maven项目&#xff0c;加入hessian依赖2. 服务端3. Stub代理4. 客户端测试输出5. rpc程序分析附 请求参数和序列化程序 6. 总结 回顾RPCRPC 序列化协议RPC 网络协议注册中心的引入dubbo框…

Milvus向量数据库可视化客户端Attu

概述 关于Milvus的介绍&#xff0c;可搜索网络资料。Milvus的使用还在摸索中&#xff1b;打算写一篇&#xff0c;时间待定。 关于Attu的资料&#xff1a; 官网GitHub文档 对于Milvus的数据可视化&#xff0c;有如下两个备选项&#xff1a; Milvus_cli&#xff1a;命令行工…

【落羽的落羽 数据结构篇】顺序结构的二叉树——堆

文章目录 一、堆1. 概念与分类2. 结构与性质3. 入堆4. 出堆 二、堆排序三、堆排序的应用——TOP-K问题 一、堆 1. 概念与分类 上一期我们提到&#xff0c;二叉树的实现既可以用顺序结构&#xff0c;也可以用链式结构。本篇我们来学习顺序结构的二叉树&#xff0c;起个新名字—…