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

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

每日激励:“不设限和自我肯定的心态: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表中的结果
附⭐⭐⭐:状态表示:通常为:以 i 位置结尾/开头,xxxxxdp[ i ] 通常通过最后一个状态来进行分析初始化 通常通过第一个状态来进行分析再做类似数字判断时,不要使用字符来简单的判断,而是将数字字符根据位数转变为真正的数字然后再进行判断,不要偷懒会容易出现不可控的情况(具体见题目4)适当的需要的情况下可以开辟空间来处理边界问题,这点在后面稍微进阶一点的动规来说就是使用的更多

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


具体训练:

1. 第 N 个泰波那契数🤓

题目🍪:

在这里插入图片描述

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

在这里插入图片描述

回顾五大步骤:

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

怎么来:经验 + 题目要求分析问题的过程中,发现子问题
  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. 填表顺序
    1. 需要在求第 i 位置那么: i - 1、i - i、i -3 三个位置的值都得算好
  4. 返回值
    1. 题目要求:第 n 个的值 dp[ n ]即可
  5. 空间优化:当我们在填某个表,某个状态只需要前面的若干个状态时,就可以使用滚动数组进行优化,本题仅需要某个数的前3个即可
    1. 本题仅需要3个变量abc来标明前3个数,求d的数

通过移动 abcd 四个数,来代替dp表

在这里插入图片描述

所以需要:从左向右

在这里插入图片描述

初始化,对本题的dp[0]、dp[1]、dp[2]这三个位置初始化,才能进行正常的开始

在这里插入图片描述

源码🗒️

classSolution{public:inttribonacci(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];}inttribonacci(int n){//空间优化写法:int a =0,b =1,c =1;int ret =0;if(n ==0)return0;if(n ==1|| n ==2)return1;for(int i =3; i <= n;i++){//填表顺序 ret = a + b + c; a = b;b = c;c = ret;}return ret;}};

2. 三步🚶问题

题目👻:

在这里插入图片描述

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

这里主要理解成:

  • 本题分析不难看出:要求到达某个台阶的方法(这里就能简单的看出来状态表示)
  • 直接拿示例一来看:如何到达台阶n=3:
    1. 也很简单:因为小孩每次可以走1/2/3步,那么那几个阶能到达呢?
    2. 无非就是 n - 1、n - 2 、n - 3 (这里n=3,故从0 1 2阶能到达这里就能看出来状态转移方程:dp[n] = dp[n-1] + dp[n-2] + dp[n-3])
    3. 那么要初始化的就是 dp[0]、dp[1]、dp[2]
    4. 从状态表示也能很简单的推出,因为本质阶梯不高:
      1. dp[0]:走到0,那么就是不用走也就是走0步,故为1
      2. dp[1]:走到1,只能从0走一步,故也为1
      3. dp[2]:走到2,那么就能从0走2步和从1走1步,所以为2
    5. 剩下就是移动方向:自然和上题一样从左往右、返回dp[n]即可

再拿走到4来看:

  • 向下的 1 2 3 个台阶,如能到达4的就是 3 2 1
  • 那么到达4的方法就是:到达 3 2 1 台阶的方法之和
在这里插入图片描述

题解核心逻辑🚶:

现在推出来了:

  1. 状态表示(经验 (以某个位置结尾/以某个位置起始) + 题目要求)
  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 ]
  3. 初始化:
    1. dp[0] = 1
    2. dp[1] = 1
    3. dp[2] = 2
  4. 填表顺序:从左往右
  5. 返回值:dp[n]

所以:dp[ i ] = dp[ i - 1 ] + dp[ i - 2 ] + dp[ i - 3 ]

在这里插入图片描述

dp[ i ] 表示到达i个台阶一共有多少个方法

在这里插入图片描述
classSolution{public:intwaysToStep(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];}//写法二:intwaysToStep(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];}//写法三://这里优化就不写了贴一份别人的:作者:SpectreintwaysToStep(int n){// 时间复杂度O(N),空间复杂度O(1)if(n ==1|| n ==2)return n;if(n ==3)return4;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. 根据最近的一步,来划分问题
    2. 先到达 i - 1,然后支付 cost[ i-1 ],走一步:dp[ i - 1] + cost[ i - 1 ]
    3. 先到达 i - 2,然后支付 cost[ i-2 ],走两步:dp[ i - 2 ] + cost[ i - 2]
    4. 所以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 开始即可
  4. 填表顺序:由题意从左往右
  5. 返回值:dp[n]

所以初始化 dp[0] = dp[1] = 0

在这里插入图片描述

用之前 或者 之后的状态,推导出dp[ i ] 的值

在这里插入图片描述
classSolution{public:intminCostClimbingStairs(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. 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 ]
  3. 初始化:
    1. dp[ n - 1 ] = cost [ n - 1 ]
  4. 填表顺序:从右往左
  5. 返回值:min(dp[0],dp[1])

dp [ n - 2 ] = cost[ n - 2 ]

在这里插入图片描述

dp[ i ] = min( dp[ i + 1 ] + cost[ i ],dp[ i + 2 ] + cost[ i ]);

在这里插入图片描述

以 i 位置为起点,xxx

在这里插入图片描述

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

中断总结🗒️一下:

  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
  3. 初始化:dp[ 0 ] 、dp[ 1 ]
  4. 填表顺序:根据状态转移方程得知从左往右
  5. 返回值:dp[ n - 1 ],n - 1最后一个位置

dp[ i ] 与 dp[ i - 1 ] 结合解码( 10 ~ 26 ) :解码成功(dp[ i - 2 ] 本质就是拼接两个新字母),失败则为0

在这里插入图片描述

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

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

  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 ] 所以不会有问题
  2. 注意下标的映射关系,在真正使用dp时,因为dp表是多一格,对于s字符串中的下标要 - 1:s[i-1]

但dp[ 0 ] 就需要我们自己控制,回顾状态转移方程,这里dp[ i - 2 ]的作用是当 i 和 i - 1结合成功时取的值,所以填1(因为前面也没字符判断了,所以填1给到dp[2],代表正确)

在这里插入图片描述
classSolution{public:int dp[101]={};intnumDecodings(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)return0;}return dp[s.size()];}}; 解法二: classSolution{public:intnumDecodings(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')return0;//处理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];}};

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

Read more

Flutter for OpenHarmony:puppeteer 远程控制 Chrome 浏览器,实现截图与自动化操作(Headless Chrome 适配) 深度解析与鸿蒙适配指南

Flutter for OpenHarmony:puppeteer 远程控制 Chrome 浏览器,实现截图与自动化操作(Headless Chrome 适配) 深度解析与鸿蒙适配指南

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net 前言 puppeteer 是一个 Node.js 库的 Dart 移植版,它提供了一套高级 API 来通过 DevTools 协议控制 Chrome 或 Chromium。通常用于爬虫、生成 PDF、截图或自动化测试。 在 OpenHarmony 移动设备上,直接启动一个 Headless Chrome 进程是不现实的(受限于系统权限和架构)。但是,我们可以利用 puppeteer 的远程连接能力,让 OpenHarmony 应用控制部署在服务器或局域网 PC 上的浏览器实例,实现强大的远程自动化功能。本文将介绍如何在 OpenHarmony 环境下使用 puppeteer 连接并控制远程浏览器。 一、puppeteer

By Ne0inhk
把服务器“装进口袋“!用 QQ 私聊打造全自动化运维助手

把服务器“装进口袋“!用 QQ 私聊打造全自动化运维助手

欢迎来到我的博客,代码的世界里,每一行都是一个故事 🎏:你只管努力,剩下的交给时间 🏠 :小破站 把服务器"装进口袋"!用 QQ 私聊打造全自动化运维助手 * 前言:那些凌晨盯着屏幕的日子 * 这套方案能做什么? * 第一步:新建一个专属的运维 Bot * 第二步:沙箱配置与权限设置 * 第三步:在 Lighthouse 面板配置 QQ 通道与技能 * 第四步:用 Markdown 文件给 Bot 定制运维人格 * 实战一:自然语言查服务器状态 * 查 CPU 使用率 * 查内存占用 * 查整机配置 * 实战二:文件操作与危险操作的双重保险 * 读取文件内容 * 删除操作:AI 强制要求二次确认 * 实战三:发现异常进程并一键处理 * 实战四:错误日志分析—

By Ne0inhk
Linux中的patch和diff命令完全指南

Linux中的patch和diff命令完全指南

🔥作者简介: 一个平凡而乐于分享的小比特,中南民族大学通信工程专业研究生,研究方向无线联邦学习 🎬擅长领域:驱动开发,嵌入式软件开发,BSP开发 ❄️作者主页:一个平凡而乐于分享的小比特的个人主页 ✨收录专栏:Linux,本专栏目的在于,记录学习Linux操作系统的总结 欢迎大家点赞 👍 收藏 ⭐ 加关注哦!💖💖 Linux中的patch和diff命令完全指南 目录 1. 什么是diff和patch? 2. diff命令详解 3. patch命令详解 4. 实战场景应用 5. 最佳实践与技巧 什么是diff和patch? diff和patch是一对相辅相成的工具,用于比较文件差异和应用补丁。简单来说: * diff:比较两个文件/目录的差异,生成补丁文件 * patch:将diff生成的补丁应用到原文件上 原文件 old.txt diff命令 新文件 new.txt 补丁文件 patch.diff patch命令

By Ne0inhk
手搓简易 Linux 进程池:从 0 到 1 实现基于管道的任务分发系统

手搓简易 Linux 进程池:从 0 到 1 实现基于管道的任务分发系统

🔥草莓熊Lotso:个人主页 ❄️个人专栏: 《C++知识分享》《Linux 入门到实践:零基础也能懂》 ✨生活是默默的坚持,毅力是永久的享受! 🎬 博主简介: 文章目录 * 前言: * 一. 核心设计思路 * 二. 代码模块拆解 * 2.1 任务定义与随机任务生成 * 2.2 子进程任务处理逻辑 * 2.3 通道(Channel)类:封装父子进程通信 * 2.4 进程池(ProcesspPool)类:核心管理逻辑 * 2.5 主函数:进程池使用示例 * 三. 关键知识点解析 * 3.1 管道通信原理 * 3.2 轮询负载均衡 * 3.3 进程回收的坑

By Ne0inhk