【算法】一文带你从浅至深入门dp动态规划
文章目录
- 一、前言
- 二、动态规划理论基础
- 1、基本概念
- 2、动态规划五部曲【✔】
- 3、出错了如何排查?
- 三、实战演练🗡
- 0x00 斐波那契数
- 0x01 第N个泰波那契数
- 0x02 爬楼梯
- 0x03 三步问题
- 0x04 使用最小花费爬楼梯⭐
- 解法一
- 解法二
- 0x05 解码方法*
- 四、总结与提炼
一、前言
本文要为大家带来的是dp动态规划,相信这是令很多同学头疼的一个东西,也是在大厂面试中很喜欢考的一个经典算法
🔰 本文总共会通过四道题来逐步从浅至深地带读者逐步认识dp动态规划
二、动态规划理论基础
首先在讲解题目之前,我们要先来说说动态规划理论基础,让大家知道到底什么是【动态规划】
1、基本概念
动态规划,英文:Dynamic Programming,简称DP,如果某一问题有很多重叠子问题,使用动态规划是最有效的。
- 如果读者有学习过【贪心算法】的话,就可以知道其和动态规划是很类似的,但是呢却有着本质的区别,对于 贪心 而言,是 局部直接选最优,但是对于 动规 而言则是 通过上一个状态去推导出下一个状态
例如:有N件物品和一个最多能背重量为W 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大
- 本题若是使用 动态规划 来做,需要先表示出数组
dp
的状态,通过dp[j-weight[i]]
推导出dp[j]
,然后得出 状态转移方程max(dp[j], dp[j - weight[i]] + value[i])
,才能计算出最终的结果 - 但若是使用 贪心算法 来求解的话,直接在每次拿物品选一个最大的或者最小的就完事了
💬 所以大家在做题的时候只要牢牢记住它们而言的最本质区别即可,题目刷多了,自然也就很好区分
2、动态规划五部曲【✔】
清楚了动态规划的基本概念后,我们再来看看在解决动态规划的问题时的解题步骤👇
- 做动规题目的时候,很多同学会陷入一个误区,就是以为把 状态转移公式 背下来,照葫芦画瓢改改,就开始写代码,甚至把题目AC之后,都不太清楚dp[i]表示的是什么
- 所以遇到一些同种类的其他题目时,就会手足无措,状态转移公式(递推公式)是很重要,但动规不仅仅只有递推公式。对于动态规划问题,我将拆解为如下五步曲:
- 确定dp数组(dp table)以及下标的含义
- 确定递推公式(状态转移方程)
- dp数组如何初始化
- 确定遍历顺序,对dp数组进行填表
- 确认返回值 / 输出内容
一些同学可能想为什么要先确定递推公式,然后再考虑初始化呢?
- 因为一些情况是递推公式决定了dp数组要如何初始化!
所以我们要先根据dp的状态,来推导出状态转移方程,接着再通过去分析这个方程,然后推敲出dp数组要怎样去做一个初始化才比较合适,不会导致越界的问题。所以对于那些只知道记公式但是不知道如何初始化和遍历数组的同学,就会显得尴尬
3、出错了如何排查?
相信大家在写代码的时候一定会出现各种各样的Bug,那要如何去解决呢,还记得之前的 LeetCode转VS调试技巧 吗?
- 但是对于动态规划而言,就是在力扣中 把dp数组打印出来,看看究竟是不是按照自己思路推导的!。这是最简洁明了的方式,所以我们遇到问题后不要害怕,而是要逐步地去分析、排查问题,加强自己排查错误的能力,如果还是不会了再去请教别人,但是在请教之前也先请问问自己下面的三个问题:
- 这道题目我举例推导状态转移公式了么?
- 我打印dp数组的日志了么?
- 打印出来了dp数组和我想的一样么?
- 把上面三个问题想明白之后再去问别人,即经过了自己的思考之后,有了自己的见解,这样才能更有效果,并且做到事半而功倍
三、实战演练🗡
在了解了一些动态规划的基础理论之后,我们还是要进入实际的问题
0x00 斐波那契数
原题传送门:力扣509.斐波那契数
首先第一道动态规划的入门题必须选择的就是【斐波那契数】,下面我会给出三种解法
- 首先是最简单的一种,即 递归解法,不做详细展开,不懂可以看看 C语言函数章节 中的函数递归,就会很清楚了
class Solution {
public:int fib(int n) {if(n < 2) return n;return fib(n - 1) + fib(n - 2);}
};
接下去的话就是我们本题的重点即 动态规划 的解法
- 首先的话就是要去创建dp数组,这里使用到的是 C++STL之vector类,如果有不懂的同学可以去看看,我们初始化出一个大小为
n + 1
的数组
vector<int> dp(n + 1);
- 接下去我们要去做的就是要去推导出这个 状态表示方程,那对于斐波那契数列我们很熟悉,后一个数等于前两个数之和,而且后面的数依赖于前面的数,所以我们的遍历数组也应该是
从左向右
dp[i] = dp[i - 2] + dp[i - 1];
- 再者我们就需要通过上面这个 状态表示方程 来对这个
dp
数组去做一个初始化的工作,此时我们需要去考虑的就是这个越界的问题,因为当前的dp[i]
依赖的是前两个数,所以若此刻的i == 0
的话,前两个数就会发生越界的情况;若是i == 1
,第一个数就会发生越界,所以 我们要对前两个数做一个初始化的操作
dp[0] = 0, dp[1] = 1;
- 那在初始化完成之后,我们就可以去做遍历填表的工作了,因为前两个数字已经被初始化了,所以从下标为
2
的地方开始向后遍历
for(int i = 2; i <= n; ++i)
{dp[i] = dp[i - 2] + dp[i - 1];
}
- 当遍历填表结束后,因为所求的是第N个斐波那契数,所以我们就需要去返回
dp[n]
return dp[n];
以下即为整体代码展示
class Solution {
public:int fib(int n) {if(n <= 1) return n;// 1.创建dp表vector<int> dp(n + 1);// 2.初始化dp[0] = 0, dp[1] = 1;// 3.遍历填表for(int i = 2; i <= n; ++i){dp[i] = dp[i - 2] + dp[i - 1];}// 4.返回值return dp[n];}
};
读者可以通过执行结果来看看dp动态规划解法的优势
再下面一种方法则是通过 滚动数组 的形式进行求解
- 可能读者有所不太理解什么是【滚动数组】,如果你了解迭代算法的话就可以知道,其实并不复杂。原理也是一样的,我们直接通过定义一维数组
int dp[2];
- 然后将上面dp动态规划中的 递推公式 转换成迭代的形式即可
for(int i = 2; i <= n; ++i)
{sum = dp[0] + dp[1];// 迭代dp[0] = dp[1];dp[1] = sum;
}
其余的没有大变化,代码如下:
class Solution {
public:int fib(int n) {if(n <= 1) return n;int sum = 0;int dp[2]; // 一维数组模拟dp[0] = 0, dp[1] = 1;for(int i = 2; i <= n; ++i){sum = dp[0] + dp[1];// 迭代dp[0] = dp[1];dp[1] = sum;}return dp[1]; // 最后累加的结果存入了dp[1]}
};
稍做了一些优化,看看运行效率
0x01 第N个泰波那契数
原题传送门:力扣1137.第 N 个泰波那契数
看完斐波那契数之后,我们再来看一个东西叫做【泰波那契数】,不知读者有否听说过呢?
1、题目解读
首先我们来解读一下本题的意思🔍
- 相信读者在看到【泰波那契数】的时候,不禁会联想到【斐波那契数】,它们呢是一对孪生兄弟,这个 泰波那契数 相当于是 斐波那契数 的加强版
- 我们首先可以来看到这个递推公式
Tn+3 = Tn + Tn+1 + Tn+2
,读者可能看不太懂,我们将其做一个转换为Tn = Tn-1 + Tn-2 + Tn-3
,即把所有n都统一-3
。那么第N个泰波那契数就等于前面3个泰波那契数的和
- 看到上面的T3,就是前3个数的和等于2,以此类推T4就是
T1 + T2 + T3 = 4
2、解题方法
看完了上面对于题目的分析之后,下面我将介绍两种解法
① dp动态规划
首先的话就是本题需要掌握的重点即【动态规划】的解法,我们要分成五步去求解
- 状态表示
- 首先读者要清楚的是在求解动态规划的题目时,都是需要一个
dp
数组的,那么对于【状态表示】的含义就是dp表里的值所表示的含义
那这个状态表示是怎么来的呢?
① 第一个呢就是按照题目要求来,即dp[i]
表示的就是第i个泰波那契数列的值
② 第二个呢则是需要读者有丰富的刷题经验,可以读完题目之后就得出对应的结果
③ 第三个呢则是在分析问题的过程中,发现重复的子问题
如果读者之前有接触过类似的动态规划问题的话,就会看到一些题解里讲:这道题的 状态表示 是怎样的,然后就直接讲本题的 状态表示方程,根本没有说这道题的状态表示是怎么来的。这个得来的过程我会在其他动态规划的题目中进行讲解
👉 所以读者在解类似的问题时一定要知道下面的这个【状态表示方程】是怎么来的,做到 “ 知其然,知其所以然 ”
- 状态表示方程
- 那么本题的状态表示方程为
dp[i] = dp[i - 1] + dp[i - 2] + dp[i - 3]
- 初始化
- 在清楚【状态表示方程】该如何写之后,我们要去做的就是对这个dp数组做一个初始化的工作。看到下面的这个dp数组,如果在一开始我们的下标取值就到
0
的话,那么i - 1
、i - 2
、i - 3
这些就会造成 越界
- 因此我们要给这个dp数组去做一个初始化,具体的就是对前三个数据即
dp[0]
、dp[1]
、dp[2]
分别初始化为【0】【1】【1】,那我们在后面遍历计算的时候就只需要从下标为3的位置开始即可
dp[0] = 0, dp[1] = dp[2] = 1;
- 填表顺序
- 接下去的话就是把dp数组按照 状态表示方程 给填充好即可
for(int i = 3; i <= n; ++i)
{// 状态转移方程dp[i] = dp[i - 1] + dp[i - 2] + dp[i - 3];
}
- 返回值
- 最后一块我们处理返回值,根据题目要求我们是要返回【第 n 个泰波那契数 Tn 的值】,所以直接
return dp[n]
即可
但是呢,若只考虑上面的这一些,在提交的时候是会出现越界的情况,因为在题目中给出的n的范围为
[0, 37]
,因此对于dp[0]
还好说,但对于后面的数据就会出现越界的情况
因此我们还需要去考虑一些边界的问题
// 边界问题处理
if(n == 0) return 0;
if(n == 1 || n == 2) return 1;
👉 整体代码会在最后给出
② 迭代优化✔
看完上面这一种,我们再来看看其是否可以去做一个优化
- 如果读者有接触过迭代算法的话,应该很快能想到本题的思路,因为是三个三个去做的累加,所以我们在这里可以定义三个变量
a
、b
、c
,它们累加后的值可以放到变量d
中
- 因此在累加完一轮之后,我们就需要去做一个迭代的操作
a = b; b = c; c = d;
- 那么在最后我们所需要返回的值就是这个
d
return d;
3、复杂度
- 时间复杂度:
对于第一种dp的解法,其时间复杂度为 O ( n ) O(n) O(n),而对于第二种迭代的解法时间复杂度为 O ( 1 ) O(1) O(1)
- 空间复杂度:
对于第一种dp的解法,其空间复杂度为 O ( n ) O(n) O(n),而对于第二种迭代的解法时间复杂度为 O ( 1 ) O(1) O(1)
👉 所以就这么对比下来迭代优化的方法还是值得大家去掌握的
4、Code
首先是第一种dp动态规划的解法
class Solution {
public:int tribonacci(int n) {// 边界问题处理if(n == 0) return 0;if(n == 1 || n == 2) return 1;// 1.创建dp表vector<int> dp(n + 1);// 2.初始化dp[0] = 0, dp[1] = 1, dp[2] = 1;// 3.填表for(int i = 3; i <= n; ++i){// 状态转移方程dp[i] = dp[i - 1] + dp[i - 2] + dp[i - 3];}// 4.返回值return dp[n];}
};
然后的话是第二种利用迭代优化的方法
class Solution {
public:// 空间优化int tribonacci(int n) {// 特殊情况处理if(n == 0) return 0;if(n == 1 || n == 2) return 1;int a = 0, b = 1, c = 1, d = 0;for(int i = 3; i <= n; ++i){d = a + b + c;// 迭代a = b; b = c; c = d;}return d;}
};
0x02 爬楼梯
原题传送门:力扣70.爬楼梯
我们再来看一道和斐波那契数很像的题目,看了代码后你就会有这样的感觉
- 题目意思很简单,若是你现在在爬楼梯的话,每次只能上1 / 2个台阶,请问上到第N个台阶有多少种不同的方法。这个其实也和【青蛙跳台阶】非常得类似,不过在那个时候我们是使用 递归 来进行求解的,这里我们要考虑使用
dp动态规划
- 不必多解释,直接上代码,这里唯一要注意的一点就是在初始化的时候是要去初始化
dp[1]
和dp[2]
,而不是去初始化dp[0]
,因为台阶并没有0号台阶,而是从1号开始
class Solution {
public:int climbStairs(int n) {if(n <= 2) return n;vector<int> dp(n + 1);dp[1] = 1; // 从第一层楼梯开始初始化dp[2] = 2;for(int i = 3; i <= n; ++i){dp[i] = dp[i - 2] + dp[i - 1];}return dp[n];}
};
0x03 三步问题
原题传送门:面试题0801.三步问题
看完了爬楼梯之后,我们再来做一个进阶,解决一个三步问题
- 首先来看一下题目所描述的意思,刚才我们一次是只能上1阶、2阶,现在的话是可以上3阶了,那么爬楼梯的种类就会增多
- 那我们先来分析一下,到
[1]
号台阶有1种方法,到[2]
号台阶有2种方法,分别是1 1
和0 2
,到[3]
号台阶则是有1 1 1
、0 2 1
、0 1 2
、0 3
,可以看做是【1 + 1 + 2 = 4】,那么以此类推到达第4号台阶即为【1 + 2 + 4 = 7】
那分析完题目后我们就要根据动规五部曲来完成对题目的分析
- 首先第一步就是需要去确定状态表示,那经过我们的分析,本题的
dp[i]
表示的就是 到达 i 位置时,一共有多少种方法
- 接下去呢我们就需要通过这个i位置的状态以及dp数组的含义,去进一步划分思考问题 。那根据题目中来看,因为是需要走三步,我们从
i
位置往前推出i - 3
、i - 2
、i - 1
这三个位置
- 那么从这三个位置到下标为
i
的位置即为dp[i - 1]
、dp[i - 2]
、dp[i - 3]
- 很明显我们就可以推出状态转移方程为:
dp[i] = dp[i - 1] + dp[i - 2] + dp[i - 3]
- 接下去呢我们就需要去考虑初始化的问题了,主要是要考虑到 越界 这一块的问题,这个我在讲解前面的题目时也有讲到过,因为不存在0号台阶,所以当此时的
i == 1
的话,前面的三个数就会发生越界的问题
- 那我们就可以对这三个位置去做一个初始化的工作了
- 接下去我们要来确立的就是填表顺序了,上面说到过这个
i
位置的值依赖于前3个值,所 以我们的填表顺序就需要【从左往右】来进行
- 那么最后的返回值即为
dp[n]
- 根据上面的五部曲,我们先来看看代码该如何去进行书写
class Solution {
public:int waysToStep(int n) {// 1.创建dp表vector<int> dp(n + 1);// 2.初始化dp[1] = 1, dp[2] = 2, dp[3] = 4;// 3.填表for(int i = 4; i <= n; ++i){dp[i] = dp[i - 1] + dp[i - 2] + dp[i - 3];}// 4.返回值return dp[n];}
};
- 首先第一个问题就是很明显的越界问题,反应快的同学应该很快就可以察觉到时前面三个位置的问题
- 所以我们应该在最前面加上这样的一些判断
// 考虑越界问题
if(n == 1 || n == 2) return n;
if(n == 3) return 4;
- 但是呢,在提交之后发现还有错误存在:仔细观察的话发现这是一个 运行时异常,叫做
signed integer overflow —— 有符号数的整数溢出
原题:结果可能很大,你需要对结果模1000000007
- 如果读者对题目本身还有印象的话就可以知道本题的结果可能会很大,所以题目中讲到要我们去做【取模】的运算
这里我们先去定义一个常量,因为对于1000000007
它的值的即为1e9 + 7
// 定义常量
const int MOD = 1e9 + 7;
然后在填表遍历的时候就可以去对所计算出来的值做一个取余的操作
dp[i] = ((dp[i - 1] + dp[i - 2]) % MOD + dp[i - 3]) % MOD;
以下即为整体的代码展示:
class Solution {
public:int waysToStep(int n) {// 定义常量const int MOD = 1e9 + 7;// 考虑越界问题if(n == 1 || n == 2) return n;if(n == 3) return 4;// 1.创建dp表vector<int> dp(n + 1);// 2.初始化dp[1] = 1, dp[2] = 2, dp[3] = 4;// 3.填表for(int i = 4; i <= n; ++i){dp[i] = ((dp[i - 1] + dp[i - 2]) % MOD + dp[i - 3]) % MOD;}// 4.返回值return dp[n];}
};
然后就看到很顺利地通过了
0x04 使用最小花费爬楼梯⭐
原题传送门:746.使用最小花费爬楼梯
知道了怎么去爬楼梯之后,我们再来做一个进阶:如何利用最小的花费去爬楼梯,本题很锻炼大家的dp思维,准备好,发车了🚗
- 首先我们来分析一下题目的意思,就是我们在向上爬楼梯的时候,需要去支付一定的费用;可以选择从 下标为 0 或下标为 1 的台阶开始爬楼梯,题目要我们计算的就是 怎样去爬才可以使得爬到楼顶所需要花费的最少
- 在这里读者尤其要注意的一个点是关注楼顶在哪里,仔细看示例就可以发现楼顶应该是在
cost[n]
的位置才对
解法一
接下去我们就可以开始通过一步一步地去进行求解
- 首先还是一样,我们要去定义dp数组并且了解这个
dp[i]
所表示的含义是什么,即到达i位置时的最小花费
- 接下去呢,我们就要通过当前的这个
i
的位置之前或者之后的状态去推导出【状态转移方程】,来表示dp[i]
- 因为我们可以选择向上爬一个或者两个台阶,所以就将求解
dp[i]
转换为了两个子问题:- 先到达
i - 1
位置,然后支付cost[i - 1]
,走一步 - 先到达
i - 2
位置,然后支付cost[i - 2]
,走二步
- 先到达
- 可以看到,因为我们在到达一个台阶时,需要支付完一笔费用后才能前行,那么前者可以转换为
dp数组 —— 到达 i 位置的最小花费来表示
;后者的话则可以转换为cost数组 —— 从每个台阶上爬需要支付的费用
- 虽然是分成了两个子问题来进行求解,但是呢题目给我们的要求是最小花费,所以我们应该要取的是二者之中的较小值。那么【状态转移方程】即为
dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
- 那么当这个【状态转移方程】写出来后我们就要去考虑这个 初始化 的问题了,因为上
0号
和1号
台阶的时候我们是不需要支付任何费用的,并且为了防止不越界,我们在初始化的时候应该令dp[0] = dp[1] = 0
- 接下来的话就要去考虑我们的填表顺序了,因为
dp[i]
是依赖于dp[i - 1]
和dp[i - 2]
,所以我们需要先算出前面的2个数才能去得到这个dp[i]
,因此这个填表的顺序应该是 从左向右
- 最后的话就是我们的返回值
dp[n]
了
以下就是代码了
class Solution {
public:int minCostClimbingStairs(vector<int>& cost) {int n = cost.size();if(n <= 1) return n;// 1.定义dp数组vector<int> dp(n + 1);// 2.初始化dp[0] = dp[1] = 0;// 3.填充dp数组for(int i = 2; i <= n; ++i){dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);}// 4.返回结果return dp[n];}
};
以下是提交后的结果
解法二
看完解法一之后,我们再来看看解法二
- 首先的话还是一样,我们要去确定这个 状态表示,在解法二中,
dp[i]
表示的从i位置出发,到达楼顶时的最小花费
- 那么接下去就慢慢地推导这个【状态转移方程】,再来回顾一下解法一
dp[i]
表示为到达i位置时的最小花费
- 那此时我们从
i
位置出发也可以分为两种- 支付当前台阶所需费用,并向后走一步,从
i + 1
位置到终点 - 支付当前台阶所需费用,并向后走二步,从
i + 2
位置到终点
- 支付当前台阶所需费用,并向后走一步,从
- 将上面的两个分析式转换为公式的话为
cost[i] + dp[i + 1]
cost[i] + dp[i + 2]
- 那么最后的这个【状态转移方程】即为
dp[i] = min(cost[i] + dp[i + 1], cost[i] + dp[i + 2]);
接下去我们需要考虑的就是这个初始化的问题,首先读者要清楚的是我们在开这个
dp
数组的时候大小应该是多大呢?是n
呢,还是n + 1
呢?
- 立即揭晓答案:应该是
[n]
,为什么?原因就在于我们到达n
级台阶的时候是不需要支付任何费用的,即dp[n] = 0
,所以去开出这个空间也是浪费的,所以这一块地方应该是作为 楼梯顶部 才对 - 那么从
dp[n - 1]
到这个顶部的位置所需要支付的费用即为cost[n - 1]
,那么从这个dp[n - 2]
到这个顶部的位置所需要支付的费用即为cost[n - 2]
- 接下去我们所需要考虑的就是 填表顺序,通过【状态转移方程】很明显可以看出我们需要先获取到
dp[i + 1]
、dp[i + 2]
的值,才可以去推导出dp[i]
的值,所以我们的填表顺序应该是从右往左的才对
- 那最后需要考虑的就是返回值了,在本解法中,我们的
dp
数组表示的是 从第i
个位置出发到达楼顶的最小花费 - 因为我们可以选择从第
[0]
或第[1]
个位置出发,所有最后的结果就是取这两个位置所需花费的最小值
以下是代码:
class Solution {
public:int minCostClimbingStairs(vector<int>& cost) {int n = cost.size();// 1.定义dp数组vector<int> dp(n);// 2.初始化【防止越界】dp[n - 1] = cost[n - 1], dp[n - 2] = cost[n - 2];// 从后往前遍历for(int i = n - 3; i >= 0; --i){dp[i] = min(dp[i + 1] + cost[i], dp[i + 2] + cost[i]);}return min(dp[0], dp[1]);}
};
来看看优化后的效率
0x05 解码方法*
力扣91. 解码方法
最后再来一道压轴题,本题是力扣里中等难度的题目。AC本题,可以让你对dp的理解进一步提升
- 首先的话我们来分析一下题目所表示的含义:在题目中会给到一个只包含数字的非空字符串
s
,然后要我们去对这个字符串做解码的操作,规则如下'A' -> "1"
'B' -> "2"
'C' -> "3"
'Z' -> "26"
- 根据上面的解码规则我们进一步来看题目要求,因为这个字符串中所包含的不仅仅只有一个数字,所以在解码的时候便需要涉及到多种的可能性,最后所需要我们返回的也是解码方法的总数
了解了题目的基本规则后,我们通过分析示例来进一步理解题目
- 首先看到示例1,
s = “12”
,那我们的解码方式就有 两种,一种是“A B”(1,2)
,一种则是L(12)
- 然后看到示例2,
s = “226”
,那我们的解码方式就有 三种,一种是“B B F”(2,2,6)
,第二种是V F(22,6)
,第三种呢则是B Z(2,26)
- 然后看到示例3,
s = “06”
,对于这个而言其实是存在问题的,因为“06”
是无法映射到【F】的,只有“6”
才可以映射到【F】,所以对于这种并不存在解码的方式
好,当我们分析完题目中所给到的示例后,就需要通过dp动态规划来解决本题,接下去就是算法原理的讲解
- 首先第一步,还一样定义出一个dp数组,然后将其状态表示出来,这里的
dp[i]
则表示 以 i 位置为结尾时的解码方法总数
- 然后接下去我们就要根据这个状态来推导出状态转移方程。我们通过这个dp数组来进行观察分析,此处我们考虑到两种情况,一个是就对
i
这个位置去做一个解码,第二种呢则是i - 1
和i
这两个位置去结合,结合完之后再去做解码的操作
- 那这个时候有同学就会有疑问了,为什么不考虑
i + 1
这个位置呢?这个的话你就要反上去继续看我们所讲到的这个状态表示了。因为我们考虑的是 以 i 位置为结尾时的解码种数,对于后面的情况此时还没有考虑到,所以呢我们不需要去理会i + 1
这个位置
- 此时我们在求解
dp
数组的时候就就分为以下两种情况下:s[i]
去单独解码,分为两种情况,那对于单独的一个数其取值就有1 ~ 9
的可能性,如果这个数为0
的话则表示解码失败s[i - 1]
与s[i]
合并去解码的话,我们就需要去考虑两个数了,第一个数要比10
来得大,否则的话就要出现我们上面所讲到的06
这种情况,第二个数的话要比26
来的小,若二者都满足这种情况的话,则可以解码成功;否则的话就表示解码失败
- 那我们对
i
这个位置单独去做解码的话,其实就相当于把[0, i - 1]
解码的所有的方案数后面统一加一个字符就可以了,具体如下图所示👇 - 那我们要去找以
i - 1
为结尾的解码总数就可以表示为dp[i - 1]
- 接下去我们再来考虑第二种情况,即我们要考虑到的是两位数合并后的情况,那所需要求解的便是
[0, i - 2]
这段区间中的解码总数,即dp[i - 2]
以下即为我们分析所得出的【状态转移方程】
接下去我们就来考虑初始化的问题
- 首先要初始化的位置相信读者在看了这么多道题之后应该很快就能反应过来了,我们在初始化的时候应该去初始化
dp[0]
和dp[1]
这两个位置的值,对于dp[0]
来说,我们是对单个位置上的数去做一个解码,那出现的情况也就只有【0】和【1】两种,数据的范围要在0 ~ 9
之间 - 那对于
dp[1]
来说,我们要对合并的两数去做一个解码,那此时就会存在3种情况[0]
即为这二者都不存在的时候[1]
即为这二者中只有一者存在的情况[2]
即为二者都存在的情况
接下去我们再来看填表顺序
- 这一块很好理解,通过我们的【状态转移方程】可以看出后面的数依赖于前面的数,那么遍历的顺序即为
从左往右
dp[i] = dp[i - 1] + dp[i - 2]
最后的话是关于返回值这一块,因为我们要找的是到第 i 个位置的解码总数,不过题目给出的是一个字符串,对于字符串的最后一个位置是
n - 1
,那么我们最后返回的结果dp[i - 1]
💬 由于本题代码比较得复杂,所以接下去分步骤讲解一下
- 首先我们要做的就是创建出
dp
表
// 创建dp表
int n = s.size();
vector<int> dp(n);
- 接下来要做的就是初始化工作,首先要去做的是初始化
dp[0]
,还记得我们上面分析的吗,当这个编码的范围在1 ~ 9
之间的时候,所以在这里我们可以采取一个逻辑判断,让dp[0]
只接收当前s[0]
这个字符的值不为0的情况
// 初始化dp[0]
dp[0] = (s[0] != '0');
- 接下去呢我们考虑初始化
dp[1]
,首先考虑到两数单独编码的情况,若均不为0的话则表示可以进行解码
// 1.两数单独编码
if(s[0] != '0' && s[1] != '0')dp[1] += 1;
- 接下去的话我们还需考虑两数结合的情况,因为这边给到的是字符串,所以我们在取的时候需要将其
- ‘0’
转换为数字才可以,接下去根据我们刚才所分析的,去判断这个数是否在符合的范围内,若是的话才表示可以解码
// 2.两数结合
int t = (s[0] - '0') * 10 + s[1] - '0'; // 字符串转数字
if(t >= 10 && t <= 26) dp[1] += 1;
- 其后我们才去考虑这个【填表】的事情,因为前两个位置
dp[0]
和dp[1]
已经做了初始化,所以我们从第3个位置开始即可,然后你就可以发现这个填表的情况和我们在初始化时的场景非常类似,这里就不细说了
// 填表
for(int i = 2;i < n; ++i) // 从第三个位置开始遍历
{// 单独编码的情况if(s[i] != '0') dp[i] += dp[i - 1]; // 两数结合的情况int t = (s[i - 1] - '0') * 10 + s[i] - '0';if(t >= 10 && t <= 26) dp[i] += dp[i - 2];
}
- 最后的话返回
dp[n - 1]
即可
return dp[n - 1];
以下是整体的代码展示:
class Solution {
public:
// 状态转移公式
// dp[i] = dp[i - 1] + dp[i - 2]int numDecodings(string s) {// 创建dp表int n = s.size();vector<int> dp(n);// 初始化dp[0]dp[0] = (s[0] != '0');// 处理边界情况if(n == 1) return dp[0];// 初始化dp[1]// 1.两数单独编码if(s[0] != '0' && s[1] != '0')dp[1] += 1;// 2.两数结合int t = (s[0] - '0') * 10 + s[1] - '0'; // 字符串转数字if(t >= 10 && t <= 26) dp[1] += 1;// 填表for(int i = 2;i < n; ++i) // 从第三个位置开始遍历{// 单独编码的情况if(s[i] != '0') dp[i] += dp[i - 1]; // 两数结合的情况int t = (s[i - 1] - '0') * 10 + s[i] - '0';if(t >= 10 && t <= 26) dp[i] += dp[i - 2];}return dp[n - 1];}
};
以下是执行结果
四、总结与提炼
最后来总结一下本文所学习的内容📖
- 在本文中,我们学习到了大厂面试中非常喜欢考察一种算法类型叫做【动态规划】,它也是令许多同学望而却步的绊脚石之一,希望在阅读本文只后可以让你对dp动态规划有了一个清晰的认识
- 首先我们对动态规划的基础理论有了一些认识,清楚了什么是 动态规划 以及 动态规划的五部曲
- 之后呢我们就展开对习题的讲解和分析,从最基础的【斐波那契数】开始,一直到较为复杂的【解码问题】,随着一步步地深入,不知读者是否对动态规划有了进一步的认识
以上就是本文所要介绍的所有内容,感谢您的阅读🌹
相关文章:
【算法】一文带你从浅至深入门dp动态规划
文章目录 一、前言二、动态规划理论基础1、基本概念2、动态规划五部曲【✔】3、出错了如何排查? 三、实战演练🗡0x00 斐波那契数0x01 第N个泰波那契数0x02 爬楼梯0x03 三步问题0x04 使用最小花费爬楼梯⭐解法一解法二 0x05 解码方法* 四、总结与提炼 一、…...
超简单免费转换ape到flac
1. 安装最新版的ffmpeg 2. 安装cywin环境 3. 设置path到ffmpeg export PATH$PATH:"PATH/TO/FFMPEG/BIN" 4.到ape所在的目录,执行以下命令 find . -iname "*.ape" | while read line; do fb${line::-4}; fn"$fb.flac";echo ffm…...
JavaScript混淆加密
什么是JS混淆加密? JavaScript混淆加密是一种通过对源代码进行变换,使其变得难以理解和分析的技术。它的目标是增加攻击者破解代码的难度,同时保持代码的功能不受影响。混淆加密的目的是使代码难以逆向工程,从而防止攻击者窃取知…...
Java8特性-Lambda表达式
📕概述 在Java 8中引入了Lambda表达式作为一项重要的语言特性,可以堪称是一种语法糖。Lambda表达式使得以函数式编程的方式解决问题变得更加简洁和便捷。 Lambda表达式的语法如下: (parameters) -> expression (参数) -> {代码}其中&…...
通过Power Platform自定义D365CE业务需求 - 1. Microsoft Power Apps 简介
Microsoft Power Apps是一个趋势性的、无代码和无代码的商业应用程序开发平台,配有一套应用程序、服务和连接器。其数据平台为构建适合任何业务需求的自定义业务应用程序提供了快速开发环境。随着无代码、少代码应用程序开发的引入,任何人都可以快速构建低代码应用程序,并与…...
简易实现QT中的virtualkeyboard及问题总结
文章目录 前言:一、虚拟键盘的实现综合代码 二、为什么选用QWidget而不适用QDialog实现键盘三、从窗体a拉起窗体b后,窗体b闪退问题的探讨四、关闭主窗口时子窗口未关闭的问题 前言: 本文章主要包含四部分: 虚拟键盘的实现&#…...
景联文科技可为多模态语音翻译模型提供数据采集支持
8月22日Facebook的母公司Meta Platforms发布了一种能够翻译和转录数十种语言的人工智能模型——SeamlessM4T,可以在日常生活中或者商务交流中为用户提供更便捷的翻译和转录服务。 相较于传统的文本翻译,这项技术的最大区别在于它可以实现端到端的语音翻译…...
定时器分批请求数据
<!DOCTYPE html> <html><script>//需要分页的数组let arr [1,2,3,4,5,6,7,8,9,10]//分割数组,每页3条splitArr(arr,4)/*** 分割数组*/function splitArr(idList,size){//当前页数let num 1//共多少页let count Math.ceil(idList.length / siz…...
【华为OD机试python】报数游戏【2023 B卷|100分】
【华为OD机试】-真题 !!点这里!! 【华为OD机试】真题考点分类 !!点这里 !! 题目描述 100个人围成一圈,每个人有一个编码,编号从1开始到100。 他们从1开始依次报数,报到为M的人自动退出圈圈,然后下一个人接着从1开始报数, 直到剩余的人数小于M。 请问最后剩余的人在原先…...
【深度学习实战—6】:基于Pytorch的血细胞图像分类(通用型图像分类程序)
✨博客主页:米开朗琪罗~🎈 ✨博主爱好:羽毛球🏸 ✨年轻人要:Living for the moment(活在当下)!💪 🏆推荐专栏:【图像处理】【千锤百炼Python】【深…...
华清远见第六课程day4作业
仿照string类,完成myString 类 #include <iostream> #include <cstring>using namespace std;class myString{ private:char *str;int size; public:myString():size(10){str new char[size];strcpy(str,"");}myString(const char*s){size …...
【广州华锐互动】AR远程智慧巡检在化工行业中的应用
AR远程智慧巡检是一种基于增强现实技术的新型巡检方式,它可以利用虚拟信息和现实场景的结合,实现对设备、工艺流程等方面的实时监测和识别。在化工行业中,AR远程智慧巡检具有广泛的应用前景,可以提高生产效率和安全性。 一、设备巡…...
easyui-sidemenu 菜单 后台加载
前言 一个项目的功能较齐全,而齐全就预示着功能菜单比较长,但是现实中在不同的甲方使用中往往只需要摘取其中几项功能,所以就想到用配置菜单以满足其需求,且无需变更原始代码,查找一些资料总是似是而非或是誊抄别的什…...
Python总结上传图片到服务器并保存的两种方式
一、前言 图片保存到服务器的两种方法: 1、根据图片的 URL 将其保存到服务器的固定位置 2、根据 request.FILES.get("file") 方式从请求中获取上传的图片文件,并将其保存到服务器的固定位置 二、方法 1、图片的 URL 要根据图片的 URL 将…...
【ETH】以太坊合约智能合约逆向方案
技术角度了解区块链 区块链技术逆袭专栏 文章目录 区块链技术逆袭专栏获取合约代码逆向工具方案1方案2实操演示:获取合约代码 在反编译之前,你需要先知道如果获取编译后的字节码。 这里以 USDT 举例 eth.getCode(0xdAC17F958D2ee523a2206206994597C13D831ec7)字节码: 0x…...
C高级Day5
课后作业: rootlinux:~/shell# cat qh.sh #!/bin/bash function sum_array() {local brr($*) local sum0for i in ${brr[*]} dosum$((sum i))doneecho $sum } arr(1 2 3 4 5) result$(sum_array ${arr[*]}) echo "数组的和为: $result"#!/bin/bash fun…...
AI绘画:Midjourney超详细教程Al表情包超简单制作,内附关键词和变现方式
大家好,本篇文章主要介绍AI绘画完成表情包的制作和变现方式分享。 你还不会AI表情包制作吗?下面我们详细的拆解制作过程。跟着这个教程做出一套属于自己的表情包。 核心工具Midjourney PS,你就可以得到一套自己的专属表情包啦~ 整体制作…...
Linux dup dup2函数
/*#include <unistd.h>int dup2(int oldfd, int newfd);作用:重定向文件描述符oldfd 指向 a.txt, newfd 指向b.txt,调用函数之后,newfd和b.txt close,newfd指向a.txtoldfd必须是一个有效的文件描述符 */ #include <unistd.h> #i…...
设计模式系列-外观模式
一、上篇回顾 上篇我们主要讲述了创建型模式中的最后一个模式-原型模式,我们主要讲述了原型模式的几类实现方案,和原型模式的应用的场景和特点,原型模式 适合在哪些场景下使用呢?我们先来回顾一下我们上篇讲述的3个常用的场景。 1…...
DBeaver 下载、安装与数据库连接(MySQL)详细教程【超详细,保姆级教程!!!】
本文介绍DBeaver 下载、安装与数据库连接(MySQL)的详细教程 一、DBeaver 下载 官网下载地址:https://dbeaver.io/download/ 二、安装 1、双击下载的安装包,选择中文 2、点击下一步 3、点击我接受 4、如下勾选,…...
使用adjustText解决标签文字遮挡问题python
使用adjustText解决文字遮挡问题 1、一个例子2、adjust_text的用法使用pip install adjustText或conda install -c conda-forge adjusttext来安装adjustText。安装成功之后,首先生成随机示例数据以方便之后的演示: 1、一个例子 我们先不使用adjustText调整图像,直接绘制出原…...
[论文笔记]SiameseNet
引言 这是Learning Text Similarity with Siamese Recurrent Networks的论文笔记。 论文标题意思是利用孪生循环神经网络学习文本相似性。 什么是孪生神经网络呢?满足以下两个条件即可: 输入是成对的网络结构和参数共享(即同一个网络)如下图所示: 看到这种图要知道可能代…...
只有个体户执照,可以用来在抖音开店吗?抖店开通问题解答
我是王路飞。 在抖音开店的门槛,本身就是需要有营业执照的。 至于执照的类型,其实主要看商家自己。 如果你是新手商家,之前也没有怎么接触过电商行业,那么用个体执照在抖音开店足够用了,毕竟你要先入门,…...
微服务高可用容灾架构设计
导语 相对于过去单体或 SOA 架构,建设微服务架构所依赖的组件发生了改变,因此分析与设计高可用容灾架构方案的思路也随之改变,本文对微服务架构落地过程中的几种常见容灾高可用方案展开分析。 作者介绍 刘冠军 腾讯云中间件中心架构组负责…...
记录docker 部署nessus
1、开启容器 docker run -itd --nameramisec_nessus -p 8834:8834 ramisec/nessus 2、登录 :注意是https https://ip8843 3、修改admin密码 #进入容器 docker exec -it ramisec_nessus /bin/bash#列出用户名 /opt/nessus/sbin/nessuscli lsuser#修改密码&a…...
qt 正则表达式
以上是正则表达式的格式说明 以下是自己写的正则表达式 22-25行 是一种设置正则表达式的方式, 29-34行 : 29行 new一个正则表达式的过滤器对象 30行 正则表达式 的过滤格式 这个格式是0-321的任意数字都可以输入 31行 将过滤格式保存到过滤器对象里面 32行 将验…...
l8-d13 UNIX域套接字
一、UNIX 域流式套接字 本地地址 struct sockaddr_un { unsigned short sun_family; /* 协议类型 */ char sun_path[108]; /* 套接字文件路径 */ }; UNIX 域流式套接字的用法和 TCP 套接字基本一致,区别在于使用的协议和地址不同 UNIX 域流式套接字服务器…...
@RequiredArgsConstructor(onConstructor=@_(@Autowired))是什么语法?
这是 Lombok 语法糖写法。 在我们写controller或者Service层的时候,需要注入很多的mapper接口或者另外的service接口,这时候就会写很多的AutoWired注解 lombok提供注解: RequiredArgsConstructor(onConstructor __(Autowired))写在类上可以…...
FL Studio Producer Edition 21.0.3.3713中文完整破解版功能特点及安装激活教程
FL Studio Producer Edition 21.0.3.3713中文完整破解版是一款由Image Line公司研发几近完美的虚拟音乐工作站,同时也是知名的音乐制作软件。它让你的计算机就像是全功能的录音室,漂亮的大混音盘,先进的创作工具,让你的音乐突破想象力的限制。…...
Mybatis 动态语言 - mybatis-velocity
前面我们介绍了Mybatis动态SQL的使用;本篇我们介绍使用mybatis-velocity动态语言生成动态SQL。 如果您对Mybatis动态SQL不太了解,建议您先进行了解后再阅读本篇,可以参考: Mybatis 动态SQL – 使用if,where标签动态生成条件语句…...
广州市网站建设 骏域动力/广东全网推广
Rsyncsersync2的数据推复制(数据的快速同步,类似于实时同步):也就是说当服务器的数据发生变化,就推新数据给备份服务器。***************************************************************************特点࿱…...
微信导购网站怎么做视频教学/磁力搜索
OSI model(open system interconnection)存在的原因: 网络模型建立是为了是网络的建造者可以建造出可以相互交流和一起工作的网络,并且描述了从一个电脑上通过网络传数据到另一个网络。 1.physical层 定义了对终端系统之间的连接的…...
网站建设 网站开发 区别/外贸如何做网站推广
在同步时间服务的时候报错,信息如下.实际上我配置的时间服务器的IP就是如下的10.188.100.103,在 执行$ansible-playbook -i hosts.ini deploy_ntp.yml -u tidb -b 命令之前,时间服务器是启动了的,机器之间的时间服务也是同步了&am…...
网站建设推广怎么做/网站流量统计分析的维度包括
原文链接:http://www.phpweblog.net/AngelLee2009/archive/2009/08/16/6848.html 1.什么是模式? 模式,即pattern。其实就是解决某一类问题的方法论。你把解决某类问题的方法总结归纳到理论高度,那就是模式。 Alexand…...
wordpress 国内优化/视频互联网推广选择隐迅推
程序中为了让更直观的反映命令执行的进度,考虑使用进度条,但是asp.net中没有专门的进度条控件,在网上搜了一下,实现方法都很复杂,就自己动手做了一个,实现起来其实也很简单。后来又想了个方法,加…...
做陶瓷的公司网站/站长之家权重
卷积,池化,特征图,实战: LeNet-5分层解析: MLP原理: 较为全面的DL: RBF: RBF课件: BP算法: 感受野:转载于:https://www.cnblogs.com/maggie94/p/6742900.html...