详细介绍:【 C/C++ 算法】入门动态规划-----一维动态规划基础(以练代学式)

news/2025/9/19 17:15:19/文章来源:https://www.cnblogs.com/lxjshuju/p/19101285

在这里插入图片描述
每日激励:“不设限和自我肯定的心态:I can do all things。 — Stephen Curry”

绪论​:
本章是动态规划算法的基础入门篇,我将通过三道简单题 + 一道中等难度的一维动态规划题来带你对动态规划有个初认识,并基本了解动态规划的最基本常见的写法,只有将基本写法了解了,对后续的难的题目自然也不会毫无头绪,后续还将持续更新更多相关的动规算法,敬请期待~
————————
早关注不迷路,话不多说安全带系好,发车啦(建议电脑观看)。


动态规划️

这里通过大量练习得出下面动态规划做题步骤
简单的说动态规划理解成:某种状态的公式 + 提前求出来值的容器出当前位置的值然后放到容器中后后续使用
因为最开始的值一般是会看见的所以就能有初始值,从而启动动态规划

从上中可以主要提炼出:

  • 状态
  • 容器的重要性
  • 公式,可以换种说法:状态转移方程

这样严格的说:动态规划 = 状态定义 + 状态转移方程 + 初始条件 + 状态存储(容器)


下述步骤是通过写完下述四道题后的总结,所以同样需要道友️大量的练习沉淀最终就能对动态规划的题目有一个基本的了解和思路,同时建议这里先一眼看过在做题中不断的磨炼同时再看这里,慢慢的找感觉。


动规基本步骤

  1. 状态表示:就是dp表(容器一般是数组)中里面的dp[ i ]值的含义
    1. 通过经验 + 题目要求:通常为:以 i 位置结尾,xxxxx
    2. 分析问题的过程中,发现相同的子问题
  2. 状态转移方程:本质就是 dp[ i ] = ?也是最难的一步
    1. 通过状态表示 再结合 题目含义推导处dp[ i ]的值到底为多少
  3. 初始化
    1. 保证填表的时候不越界
    2. 主要也是根据题目进行
  4. 填表顺序
    1. 为了保证填写当前状态的时候前面状态已经存到容器中,这样才能进行计算
    2. 一般是从左向右/从上到下
  5. 返回值
    1. 通过最终得到的dp表和题意找到dp表中的结果

附⭐⭐⭐:

  1. 状态表示:通常为:以 i 位置结尾/开头,xxxxx
  2. dp[ i ] 通常通过最后一个状态来进行分析
  3. 初始化 通常通过第一个状态来进行分析
  4. 再做类似数字判断时,不要使用字符来简单的判断,而是将数字字符根据位数转变为真正的数字然后再进行判断,不要偷懒会容易出现不可控的情况(具体见题目4)
  5. 适当的需要的情况下可以开辟空间来处理边界问题,这点在后面稍微进阶一点的动规来说就是使用的更多

话不多说,从题目来以练代学式的快速上手,我也会边分析过程边使用上述的5步骤


具体训练:

1. 第 N 个泰波那契数

题目:

在这里插入图片描述

分析题目并提出,解决方法:

在这里插入图片描述

回顾五大步骤:

  1. 状态表示:就是dp表中里面的值的含义

在这里插入图片描述
怎么来

  1. 经验 + 题目要求
  2. 分析问题的过程中,发现子问题
  1. 状态转移方程:本质就是dp[ i ] 等于什么(也是最难的一步)
    1. 根据状态表示 + 题目含义得出
  2. 初始化
    1. 保证填表的时候不越界
    2. 也是根据题目意思进行填写
  3. 填表顺序
    1. 为了填写当前状态的时候,所需要的状态已经计算过了
    2. 一般是从左向右/从上到下
  4. 返回值
    1. 题目要求 + 状态表示

题解核心逻辑✍️:

  1. 状态表述:dp[ i ]:第 i 个泰波那契数
    1. 根据经验 + 题目要求
  2. 本题的转移方程: dp[ i ] = dp[ i - 1 ] + dp[ i - 2 ] + dp[ i - 3 ](通过上图总的公式直接得出)
  3. 初始化,对本题的 dp[0]、dp[1]、dp[2] 这三个位置初始化,才能进行正常的开始
    在这里插入图片描述
  4. 填表顺序
    1. 需要在求第 i 位置那么: i - 1、i - i、i -3 三个位置的值都得算好
    2. 所以需要:从左向右
      在这里插入图片描述
  5. 返回值
    1. 题目要求:第 n 个的值 dp[ n ]即可
  6. 空间优化:当我们在填某个表,某个状态只需要前面的若干个状态时,就可以使用滚动数组进行优化,本题仅需要某个数的前3个即可
    1. 本题仅需要3个变量abc来标明前3个数,求d的数
    2. 通过移动 abcd 四个数,来代替dp表
      在这里插入图片描述

源码️

class Solution
{
public:
int tribonacci(int n) {
//1. 状态表示:dp[i] = 第i个泰波那契序列
//2. 状态表示:Tn+3 = Tn + Tn+1 + Tn+2 -> dp[i] = dp[i-1] + dp[i-2] + dp[i-3]
vector<
int>
dp(n + 1);
//提前开辟好 n 个位置的空间,注意从0开始所以+1
//3. 初始化:需要前三个 那么就是初始化 0 1 2,从3开始
dp[0] = 0,dp[1] = 1,dp[2] = 1;
//4. 填表顺序:要求的值是Ti,那么求就是 dp[i],所以肯定是从小的开始,也就是从左往右
for(int i = 3;i <= n;i++)
{
dp[i] = dp[i-1] + dp[i-2] + dp[i-3];
//这里本质就是使用状态转移方程
}
//5. 返回值:dp[i]即可
return dp[n];
}
int tribonacci(int n) {
//空间优化写法:
int a = 0,b = 1,c = 1;
int ret = 0;
if(n == 0) return 0;
if(n == 1 || n == 2) return 1;
for(int i = 3 ; i <= n;i++){
//填表顺序
ret = a + b + c;
a = b;b = c;c = ret;
}
return ret;
}
};

2. 三步问题

题目:

在这里插入图片描述

分析题目并提出,解决方法:

这里主要理解成:

再拿走到4来看:

在这里插入图片描述

题解核心逻辑:

现在推出来了:

  1. 状态表示(经验 (以某个位置结尾/以某个位置起始) + 题目要求)
    1. dp[ i ] 表示到达i个台阶一共有多少个方法
      在这里插入图片描述
  2. 状态转移方程:以 i 位置的状态,最近的一步,来划分问题
    1. dp[i] 分为三种情况:
    2. 从 i - 1 -> i:dp[ i - 1 ]
    3. 从 i - 2 -> i:dp[ i - 2 ]
    4. 从 i - 3 -> i:dp[ i - 3 ]
    5. 所以:dp[ i ] = dp[ i - 1 ] + dp[ i - 2 ] + dp[ i - 3 ]
      在这里插入图片描述
  3. 初始化:
    1. dp[0] = 1
    2. dp[1] = 1
    3. dp[2] = 2
  4. 填表顺序:从左往右
  5. 返回值:dp[n]
class Solution
{
public:
int waysToStep(int n) {
//1. 状态表示:dp[n] 计算小孩上到 n 阶台阶有多少种上楼梯的方式
//2. 状态方程:dp[i] = dp[i-1] + dp[i-2] + dp[i-3] 本质还是很上题一样
vector<
int>
dp(n+3);
//这里的初始化同样要注意因为可能n较小所以需要+3
//3. 初始值:同样的 0 1 2,但本题的初始化就没那么简单了
// 也希望你能很好的体会 状态表示的作用:确定每个状态点i位置的值
dp[0] = 1;
//到达0阶有几种
dp[1] = 1;
//到达1阶有几种
dp[2] = 2;
//到达2阶有几种
if(n <= 2) return dp[n];
//4. 方向:同样需要求 i 就得先找到 i-3... 那么就得从左往右
for(int i = 3;i <= n;i++)
{
dp[i] = ((long)dp[i-1] + dp[i-2] + dp[i-3]) % 1000000007;
//这里本质就是使用状态转移方程
}
//5. 返回dp[n]即可
return dp[n];
}
//写法二:
int waysToStep(int n) {
vector<
int>
dp(n + 3);
dp[1] = 1;dp[2] = 2;dp[3] = 4;
//另外一种看法忽律0台阶直接从1开始,方法类似不过多叙述
for(int i = 4;i<=n;i++){
dp[i] = ((long)dp[i-1] + dp[i-2] + dp[i-3]) % 1000000007;
}
return dp[n];
}
//写法三:
//这里优化就不写了贴一份别人的:作者:Spectre
int waysToStep(int n) {
// 时间复杂度O(N),空间复杂度O(1)
if (n == 1 || n == 2) return n;
if (n == 3) return 4;
int dp1 = 1, dp2 = 2, dp3 = 4, dp4;
for (int i = 4; i <= n;
++i) {
dp4 = ((dp1 + dp2) % 1000000007 + dp3) % 1000000007;
dp1 = dp2;
dp2 = dp3;
dp3 = dp4;
}
return dp4;
}
};

在这里插入图片描述

3. 使用最小花费爬楼梯

题目:

在这里插入图片描述

分析题目并提出,解决方法✍️:

题解核心逻辑:

解法一:

  1. 状态表示(经验+题目要求):
    1. 以 i 位置为结尾,xxx:
    2. 所以:dp[ i ] 表示:到达 i 位置时,最小花费
  2. 状态转移方程
    1. 用之前 或者 之后的状态,推导出dp[ i ] 的值
      在这里插入图片描述
    2. 根据最近的一步,来划分问题
    3. 先到达 i - 1,然后支付 cost[ i-1 ],走一步:dp[ i - 1] + cost[ i - 1 ]
    4. 先到达 i - 2,然后支付 cost[ i-2 ],走两步:dp[ i - 2 ] + cost[ i - 2]
    5. 所以dp[ i ] = min( dp[ i - 1] + cost[ i - 1 ] , dp[ i - 2 ] + cost[ i - 2] )
  3. 初始化(防止越界,因为会用到 i - 1 和 i - 2 两个位置的值):
    1. 因为dp[ 0 ] = dp[ -1 ] + dp[ -2 ]这是没必要的,从题目可知
    2. 0 1 台阶是0元,所以直接从 2 开始即可
    3. 所以初始化 dp[0] = dp[1] = 0
      在这里插入图片描述
  4. 填表顺序:由题意从左往右
  5. 返回值:dp[n]
class Solution
{
public:
int minCostClimbingStairs(vector<
int>
& cost) {
// cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用
// 爬到楼顶本质就是超过容器个数 size=3 就需要爬到4(下标3)
// 1. dp[i]:到达第i层需要的 最小 花费
int n = cost.size();
vector<
int>
dp(n+1);
// 2. 状态转移方程: dp[i] = min(dp[i-1],dp[i-2]) + cost[i] 因为只能从下两层爬上来 + 当前需要的价值,注意的是cost[size]是越界的,此时+0即可
// 3. 初始化:最小情况: dp[2] = min(dp[1],dp[0]) + cost[2] ,故初始化dp[1] dp[2] 即可
dp[1] = cost[1];
dp[0] = cost[0];
for(int i = 2;i<= n;i++)
{
if(i == n) dp[i] = min(dp[i-1],dp[i-2]);
else dp[i] = min(dp[i-1],dp[i-2]) +cost[i];
}
return dp[n];
}
};

在这里插入图片描述

方法二:

  1. 状态表示:
    1. 以 i 位置为起点,xxx
      在这里插入图片描述
    2. dp[ i ]:从 i 位置开始,到达楼顶,此时的最小花费
  2. 状态转移方程
    1. 用之前或者之后的状态推导
    2. 支付cost[i],往后走一步,从 i + 1 位置出发到达终点 dp[ i + 1 ] + cost[ i ]
    3. 支付 cost[ i ],往后走两步,从 i + 2 位置出发到达终点 dp[ i + 2 ] + cost[ i ]
    4. dp[ i ] = min( dp[ i + 1 ] + cost[ i ],dp[ i + 2 ] + cost[ i ]);
      在这里插入图片描述
  3. 初始化:
    1. dp[ n - 1 ] = cost [ n - 1 ]
    2. dp [ n - 2 ] = cost[ n - 2 ]
      在这里插入图片描述
  4. 填表顺序:从右往左
  5. 返回值:min(dp[0],dp[1])

这里就不扩展写了,感兴趣的可以实践下

中断总结️一下:

  1. 通过上述好几道题不难总结出常用的状态表示经验:
    1. 以 i 位置为结尾,xxx
    2. 以 i 位置为开始,xxx

通过上述三道题,估计你对动态规划中的最基本的步骤和思想都有一定的认识了,下面就不将是简单的套公式了,而是结合一些内容进一步深化对动态规划的理解
也就需要你在抽象的题目中提解出动态规划~~
本质也就是:状态表示不简单了、状态方程也没那么好推导了


4. 解码方法

题目:

在这里插入图片描述
在这里插入图片描述

分析题目并提出,解决方法✍️:

在这里插入图片描述
本题如何想到的呢:从最后一个节点来看,不难发现规律:
在这里插入图片描述

  1. 要求第i位置的能否解码,就只用考略
    1. 当前位置i是否符合(0 <= i <= 9)
    2. i位置与i-1位置结合时是否符合(10 <= i-1 + i <= 26)
  2. 要求总共的解码个数,则是通过dp表存储每个节点的解码个数,最小的情况是很容易推算的,所以使用动规中的dp表是能进行存储的
  3. 通过上面两个想法也就不难得出状态转移方程如何计算
    1. 判断能否解码的两种方法,看这两种方法那个符合条件,若符合条件就记录上存储进dp表中
    2. 若不能解码的当然就不计算
    3. 具体见下面详细五步

题解核心逻辑:

  1. 状态表示:
    1. 以 i 结尾时,解法方法的总数
  2. 状态转移方程
    1. 根据最近的一步划分问题:
    2. 分为两种情况:
      1. dp[ i ]单独解码( 1 ~ 9 ):解码成功(个数为 dp[ i -1 ]:因为本质就是拼接了一个新字母这里好好理解下,并不用+1哦),失败则为0
      2. dp[ i ] 与 dp[ i - 1 ] 结合解码( 10 ~ 26 ) :解码成功(dp[ i - 2 ] 本质就是拼接两个新字母),失败则为0
        在这里插入图片描述
  3. 初始化:dp[ 0 ] 、dp[ 1 ]
  4. 填表顺序:根据状态转移方程得知从左往右
  5. 返回值:dp[ n - 1 ],n - 1最后一个位置

优化:处理边界问题以及初始化问题的技巧

整个数组多开一位,然后通过使用这个多开的一位虚拟节点的初始化来帮助运算
注意的事项:

  1. 虚拟节点的初始化是根据题目意思来的,保证后面填表的正确
    1. 一般来说填写0
    2. 但本题对于该虚拟节点的使用,为了求原dp表中的dp[ 1 ] 时使用的 dp[ i - 2 ]
    3. 因为假设 dp[ 1 ] 和 移动后就变成了 dp[ 2 ],此时要求dp[ 2 ] 需要dp[ 1 ] 和 dp [ 0 ]
    4. dp [ 1 ] 本质就是原dp数组中的dp[ 0 ] 所以不会有问题
    5. 但dp[ 0 ] 就需要我们自己控制,回顾状态转移方程,这里dp[ i - 2 ]的作用是当 i 和 i - 1结合成功时取的值,所以填1(因为前面也没字符判断了,所以填1给到dp[2],代表正确)
      在这里插入图片描述
  2. 注意下标的映射关系,在真正使用dp时,因为dp表是多一格,对于s字符串中的下标要 - 1:s[i-1]
class Solution
{
public:
int dp[101] = {
};
int numDecodings(string s) {
//初始化 0 和 1
dp[0] = 1;
//虚拟节点
if( 1 <= s[0] - '0' && s[0] - '0' <= 9) dp[1] = 1 ;
//原dp[0]
//移动s字符下标完成:
for(int i = 2; i <= s.size();i++)
{
//单个字符
int onechar = s[i - 1] - '0';
if( 1 <= onechar && onechar <= 9) dp[i] += dp[i-1];
//两个字符
int combine = (s[i - 2] - '0') * 10 + onechar;
if(10 <= combine && combine <= 26) dp[i] += dp[i-2];
if(dp[i] == 0) return 0;
}
return dp[s.size()];
}
};
解法二:
class Solution
{
public:
int numDecodings(string s) {
//dp[i]:以i位置结尾的解码个数:
//dp[i] = dp[i-1] + dp[i-2],其中若若不能解码则不进行相加,或者加0
//初始化:因为s.length >= 1,所以需要一个多的空位,来特殊处理等于1的情况
int n = s.size();
vector<
int>
dp(n+2);
//结果存在n下标
dp[0] = 1;
//默认解码个数为1次
dp[1] = 1;
//默认解码个数为1次
if(s[0] == '0') return 0;
//处理dp下标完成存储效果
for(int i = 0; i < n;i++)
{
//不要简单的字符判断!!!!!!!!!!!
// if(i-1 >= 0 && s[i-1] - '0' >= 1 && s[i-1] - '0' <= 2 && 
// s[i] - '0' >= 0 && s[i] - '0' <= 6) dp[i+2] += dp[i];
int val = s[i] - '0';
if(val >= 1 && val <= 9) dp[i+2] += dp[i+1];
int combine = i-1 >= 0 ? ( s[i-1] - '0') * 10 + val : -1;
if(combine >= 10 && combine <= 26) dp[i+2] += dp[i];
}
return dp[n+1];
}
};

对于需要判断两位或者多位数字时,不要图方便使用字符判断,而是将他转换为数字

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

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

相关文章

iOS 26 能耗检测实战指南 如何监测 iPhone 电池掉电、Adaptive Power 模式效果与后台耗能问题(uni-app 与原生 App 优化必看)

本文结合 iOS 26 新增的 Adaptive Power 模式与电池界面功能,介绍如何检测 iPhone 能耗问题(包括启动耗电、后台耗电、动画特效与资源加载耗电等),并结合 Instruments、克魔等工具给出 uni-app 项目的实战优化指南…

Transformer的个人理解

Transformer工作原理 分词 文本进来之后,首先经过Tokenizer(分词器)分割成很多个token。每个token都会赋予一个从0开始的ID,用于后续索引。 然后通过一个embedding层,将token转换成一个多维向量,也叫做embedding…

国标GB28181平台EasyGBS如何实现企业园区视频监控一体化管理?

国标GB28181平台EasyGBS如何实现企业园区视频监控一体化管理?企业园区安防系统面临多品牌设备兼容难、系统孤立、管理效率低等痛点。本文详细探讨基于国标GB28181协议的EasyGBS视频平台如何为企业园区提供完整的视频监…

360环视硬件平台为什么推荐使用米尔RK3576开发板?

在汽车智能化和智慧安防快速发展的今天,360环视系统 已成为保障行车与场景安全、提升体验的重要技术。无论是自动泊车、驾驶辅助,还是智慧社区监控,核心诉求都是能够接入 多路摄像头,并通过高效的 推流 实现低延迟…

C语言弱函数

C语言弱函数project/ ├── core/ │ └── system_init.c // 核心框架代码,包含弱函数 ├── boards/ │ └── my_board.c // 板级特定代码,提供强函数 └── main.c // 主程序 /…

高质量票据识别数据集:1000张收据图像+2141个商品标注,支持OCR模型训练与文档理解研究

​获取更多高质量数据集,请访问典枢数据交易平台:https://dianshudata.com/引言与背景 在数字化转型的浪潮中,票据识别技术已成为金融、零售、餐饮等行业自动化处理的核心技术。传统的票据处理依赖人工录入,效率低…

1202_InnoDB中一条UPDATE语句的执行流程

InnoDB中一条UPDATE语句的执行流程可以概括为以下步骤和图示:下面是每个步骤的详细说明: 第一步:连接器与权限验证 (Client → Server Layer)建立连接:客户端应用程序(如Java程序、MySQL CLI)与MySQL服务器建立连…

1201_mysql查询语句select执行流程

查询语句select执行流程,如下图所示,其核心流程可概括为: flowchart TD A["客户端提交 SELECT 语句"] --> B["连接器<br>管理连接, 权限验证"] B --> C["查询缓存<br>(…

记录---vue3项目实战 打印、导出PDF

🧑‍💻 写在开头 点赞 + 收藏 === 学会🤣🤣🤣 一 维护模板 1 打印模板:<template> <div class="print-content"> <div v-for="item in data.detailList" :key="…

node.js安装(绿色版)

1、官方下载,选择【独立文件(.zip)】下载https://nodejs.org/zh-cn/download/ 2、解压到C:\Program Files\nodejs3、在安装目录下创建以下两个目录node_global(默认:C:\Program Files\nodejs) node_cache(默认:C…

selenium完整版一览 - 教程

selenium完整版一览 - 教程pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", …

创龙 瑞芯微 RK3588 国产2.4GHz八核 工业开发板—开发环境搭建(二) - 创龙科技

本文围绕创龙科技研发的开发环境搭建需求,详细阐述了完整流程:先安装 VMware 16.2.5 虚拟机,再基于虚拟机部署 Ubuntu 20.04.6 系统,含系统配置、虚拟机工具安装及共享目录设置;还讲解了 GDB 10.2 调试工具的环境…

ctfshow web55

<?php/* # -*- coding: utf-8 -*- # @Author: Lazzaro # @Date: 2020-09-05 20:49:30 # @Last Modified by: h1xa # @Last Modified time: 2020-09-07 20:03:51 # @email: h1xa@ctfer.com # @link: https://ct…

ctfshow web58

<?php/* # -*- coding: utf-8 -*- # @Author: Lazzaro # @Date: 2020-09-05 20:49:30 # @Last Modified by: h1xa # @Last Modified time: 2020-09-07 22:02:47 # @email: h1xa@ctfer.com # @link: https://ct…

ctfshow web57

<?php /* # -*- coding: utf-8 -*- # @Author: h1xa # @Date: 2020-09-05 20:49:30 # @Last Modified by: h1xa # @Last Modified time: 2020-09-08 01:02:56 # @email: h1xa@ctfer.com # @link: https://ctfe…

详细介绍:【SQLMap】POST请求注入

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

ctfshow web52

2 <?php/* # -*- coding: utf-8 -*- # @Author: h1xa # @Date: 2020-09-05 20:49:30 # @Last Modified by: h1xa # @Last Modified time: 2020-09-05 22:50:30 # @email: h1xa@ctfer.com # @link: https://ctf…

ctfshow web51

<?php/* # -*- coding: utf-8 -*- # @Author: h1xa # @Date: 2020-09-05 20:49:30 # @Last Modified by: h1xa # @Last Modified time: 2020-09-05 22:42:52 # @email: h1xa@ctfer.com # @link: https://ctfer…