文章目录
- 509.斐波那契数列
- 思路:动规五步
- 确定dp数组和数组下标含义
- 递推公式
- DP数组初始化
- 遍历顺序
- 打印DP数组
- 完整版
- debug测试
- 空间复杂度优化版
- 优化思路
- 70.爬楼梯
- 思路
- DP数组的含义以及下标含义
- 递推公式
- DP数组初始化
- 遍历顺序
- 打印DP数组
- 完整版
- debug测试
- 空间复杂度优化写法
- 746.使用最小花费爬楼梯
- 思路
- DP数组含义
- 递推公式
- DP数组初始化
- 遍历顺序
- 遍历顺序补充
- 打印DP数组
- 完整版
- 空间复杂度优化写法
509.斐波那契数列
斐波那契数 (通常用 F(n) 表示)形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是:
F(0) = 0,F(1) = 1
F(n) = F(n - 1) + F(n - 2),其中 n > 1
给定 n ,请计算 F(n) 。
示例 1:
输入:n = 2
输出:1
解释:F(2) = F(1) + F(0) = 1 + 0 = 1
示例 2:
输入:n = 3
输出:2
解释:F(3) = F(2) + F(1) = 1 + 1 = 2
示例 3:
输入:n = 4
输出:3
解释:F(4) = F(3) + F(2) = 2 + 1 = 3
提示:
0 <= n <= 30
思路:动规五步
确定dp数组和数组下标含义
DP题目都需要定义一维或者二维的状态转移数组,通常是叫dp。
本题中,dp[i]表示第i个斐波那契数的数值为dp[i]
递推公式
本题是比较简单的DP题目,就是因为题目描述中已经把递推公式告诉我们了。
递推公式:dp[i]=dp[i-1]+dp[i-2]
DP数组初始化
题目描述已经说了,dp[0]=1
遍历顺序
因为dp[i]是由dp[i-1]和dp[i-2]得到的,因此需要从前往后遍历,才能保证每次dp[i]能够考虑到前面的两个元素。
打印DP数组
这一步主要用于debug,打印出来看看和想象的是否一样
完整版
class Solution {
public:
int fib(int n) {
if(n==0)
return 0;
if(n==1)
return 1;
//建立dp数组
vector<int>dp(n+1);
//dp数组初始化,初始化依赖于递推公式
//注意这里初始化需要放到if特殊情况后面,因为如果n是0,就不存在dp[1]
dp[0]=0;
dp[1]=1;
//递推公式
for(int i=2;i<=n;i++){
dp[i]=dp[i-1]+dp[i-2];
}
return dp[n];
}
};
- 时间复杂度:O(n)
- 空间复杂度:O(n)
debug测试
这段代码的问题出在没有处理 n
为 0 或 1 的情况。如果 n
为 0,那么 dp[1]
就不存在,这时试图访问 dp[1]
会导致溢出。
dp[0]=0;dp[1]=1;
if特殊情况需要放最前面,因为如果n是0,就不存在dp[1]
修改加上if条件之后通过。
空间复杂度优化版
- 实际上,我们只需要维护两个数值就可以了,不需要记录整个序列。
class Solution {
public:
int fib(int N) {
if (N <= 1) return N;
int dp[2];
dp[0] = 0;
dp[1] = 1;
for (int i = 2; i <= N; i++) {
int sum = dp[0] + dp[1];
dp[0] = dp[1];
dp[1] = sum;
}
return dp[1];
}
};
优化思路
斐波那契数列的定义:F(0) = 0,F(1) = 1,F(n) = F(n-1) + F(n-2) 对于所有 n >= 2。这意味着,要计算第 n 个斐波那契数,只需要知道前两个斐波那契数,即 F(n-1) 和 F(n-2)。
优化版本的斐波那契数列计算利用了这个性质。在循环开始时,dp[0] 和 dp[1] 分别存储 F(n-2) 和 F(n-1)。然后,我们计算新的斐波那契数 F(n) = dp[0] + dp[1],并更新 dp[0] 和 dp[1],以备下一个循环使用。所以,我们只需要两个变量就可以计算出斐波那契数列的下一个值,而不必维护整个数列。
这样的优化实际上是一个空间优化,称为 “滚动数组” 或者 “滑动窗口” 的策略。其基本思想是只保存当前阶段需要的数据,淘汰过去不再需要的数据,避免存储不必要的信息,从而降低空间复杂度。
- 时间复杂度:O(n)
- 空间复杂度:O(1)
70.爬楼梯
假设你正在爬楼梯。需要 n
阶你才能到达楼顶。
每次你可以爬 1
或 2
个台阶。你有多少种不同的方法可以爬到楼顶呢?
示例 1:
输入:n = 2
输出:2
解释:有两种方法可以爬到楼顶。
1. 1 阶 + 1 阶
2. 2 阶
示例 2:
输入:n = 3
输出:3
解释:有三种方法可以爬到楼顶。
1. 1 阶 + 1 阶 + 1 阶
2. 1 阶 + 2 阶
3. 2 阶 + 1 阶
提示:
1 <= n <= 45
思路
一共n阶台阶,1阶:1步,2阶:2种(2或者1+1),3阶:3种(2+1或1+2或1+1+1)4阶:5种。
我们可以发现,3阶,只能从1阶和2阶迈上来,实际上就是1阶的1种方法加上2阶的2种方法。
而4阶,只能从2阶和3阶迈上来,因此登上4阶的方法数就是登上2阶的方法数+登上3阶的方法数,2+3=5种。
我们此时就可以发现递推关系,也就是当前阶梯的状态,依赖于他的前两个阶梯的状态。(一次性最多迈两步)
也就是说,因为每次只能爬 1 级或 2级,所以f(x)的数值只能从f(x-1)和f(x-2)转移过来。而这里要统计方案总数,我们就需要对这两项的贡献求和。
DP数组的含义以及下标含义
dp[i]:达到第i阶,有dp[i]种方法。
后面的推导都是基于含义
递推公式
dp[i]=dp[i-1]+dp[i-2]
,其中dp[i-1]
表示达到第i-1阶有多少种方法,dp[i-2]同理
DP数组初始化
首先因为题目描述达到第一阶有1种方法第二阶有2种,所以dp[1]=1,dp[2]=2.
dp[0]的含义是,达到第0阶需要多少种方法。但是本题中,dp[0]是没有意义的!因为题目给出的数据范围,n是一个>=1的正整数,因此我们完全不需要考虑dp[0]的情况,也不需要像题解一样令dp[0]=1,因为没有意义。
遍历顺序
遍历顺序一定是从前往后,因为本题也属于斐波那契数列题目,当前值基于他的前两个状态。
打印DP数组
我们可以先推导自己认为的DP数组数值,然后打印看是否符合要求。
完整版
- 本题也是一道斐波那契数列的相关题目
class Solution {
public:
int climbStairs(int n) {
if(n<=2) return n;
vector<int>dp(n+1,0);
dp[1]=1;
dp[2]=2;
for(int i=3;i<=n;i++){
dp[i]=dp[i-1]+dp[i-2];
}
return dp[n];
}
};
debug测试
Line 1034: Char 34: runtime error: applying non-zero offset 4 to null pointer (stl_vector.h)
这个错误信息是说试图对一个空的vector应用非零的偏移量。这个问题出在使用 dp[i]
之前没有为 dp
分配足够的空间。
在C++中, std::vector
的初始大小为0,如果试图访问或修改不存在的元素(如 dp[1]
或 dp[2]
),这就会导致运行时错误。
需要先调用 std::vector::resize
或者在创建 std::vector
时就指定它的大小,才能保证有足够的空间来存储元素。
修改dp初始化:vector<int>dp(n+1,0)
空间复杂度优化写法
- 很多动规的题目其实都是当前状态依赖前两个,或者前三个状态,都可以做空间上的优化,但面试中能写出版本一就够了,清晰明了,如果面试官要求进一步优化空间的话,我们再去优化。
- 因为版本一才能体现出动规的思想精髓,递推的状态变化、
// 版本二
class Solution {
public:
int climbStairs(int n) {
if (n <= 1) return n;
int dp[3];
dp[1] = 1;
dp[2] = 2;
for (int i = 3; i <= n; i++) {
int sum = dp[1] + dp[2];
dp[1] = dp[2];
dp[2] = sum;
}
return dp[2];
}
};
746.使用最小花费爬楼梯
给你一个整数数组 cost ,其中 cost[i]
是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。
你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。
请你计算并返回达到楼梯顶部的最低花费。
示例 1:
输入:cost = [10,15,20]
输出:15
解释:你将从下标为 1 的台阶开始。
- 支付 15 ,向上爬两个台阶,到达楼梯顶部。
总花费为 15 。
示例 2:
输入:cost = [1,100,1,1,1,100,1,1,100,1]
输出:6
解释:你将从下标为 0 的台阶开始。
- 支付 1 ,向上爬两个台阶,到达下标为 2 的台阶。
- 支付 1 ,向上爬两个台阶,到达下标为 4 的台阶。
- 支付 1 ,向上爬两个台阶,到达下标为 6 的台阶。
- 支付 1 ,向上爬一个台阶,到达下标为 7 的台阶。
- 支付 1 ,向上爬两个台阶,到达下标为 9 的台阶。
- 支付 1 ,向上爬一个台阶,到达楼梯顶部。
总花费为 6 。
提示:
- 2 <=
cost.length
<= 1000 - 0 <=
cost[i]
<= 999
思路
本题首先要明确题意。题目中没有给出楼顶的位置,但是楼顶的阶数应该是cost.size()
而不是cost数组的最大下标。题意如下图所示。
可以选择0或者1往上跳,每次往上跳都花费cost[i]的体力。
DP数组含义
我们要求的是到达楼顶的最小花费,dp[i]表示的就是花费。
i表示的是当前到了哪个台阶,而dp[i]
的值表示的就是到i位置时候的所有花费
DP数组含义一定要搞清楚,这一点很重要,递推公式基于数组
递推公式
递推公式我们需要得到的是dp[i]。本题可以一步一个台阶或者一步两个台阶,因此,dp[i]是由dp[i-1]或者dp[i-2]跳上来的。
dp[i]表示的是,跳到i位置所需要的最小花费。因为既可以从i-1跳上来,也可以从i-2,因此递推就是取这二者花费的最小值。公式推导如下图:
因此公式为,dp[i]=min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2])
。
DP数组初始化
dp公式可以看出,最开始的dp[2]是由dp[1]和dp[0]求得。也就是说我们只需要初始化dp[1]和dp[0]。
因为0和1是初始值,往上跳的时候才需要花费体力值,因此dp[0]和dp[1]的值都是0.
(DP数组的含义:dp[i]表示的是跳到i时候的花费,初始值花费就是0)
遍历顺序
本题也是爬楼梯的衍生题目,因此也是从前到后遍历。
遍历顺序补充
但是稍稍有点难度的动态规划,其遍历顺序并不容易确定下来。 例如:01背包,都知道两个for循环,一个for遍历物品,嵌套一个for遍历背包容量,那么,为什么不是一个for遍历背包容量,嵌套一个for遍历物品呢? 以及在使用一维dp数组的时候遍历背包容量为什么要倒序呢?
这些问题都是和遍历顺序有关的,等学到了背包再进行对比。
打印DP数组
debug过程中如果出现问题,就把预期DP数组写出,再打印进行对比。
预期DP数组:
完整版
class Solution {
public:
int minCostClimbingStairs(vector<int>& cost) {
if(cost.size()<=1) return 0;
int n=cost.size();
//初始化
vector<int>dp(n+1,0);
//dp[0]=0;已经进行了0的初始化这两句可以不写
//dp[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];
}
};
- 时间复杂度:O(n)
- 空间复杂度:O(n)
空间复杂度优化写法
// 版本二
class Solution {
public:
int minCostClimbingStairs(vector<int>& cost) {
int dp0 = 0;
int dp1 = 0;
for (int i = 2; i <= cost.size(); i++) {
int dpi = min(dp1 + cost[i - 1], dp0 + cost[i - 2]);
dp0 = dp1; // 记录一下前两位
dp1 = dpi;
}
return dp1;
}
};
- 时间复杂度:O(n)
- 空间复杂度:O(1)
- 在面试中,能写出版本一就行,除非面试官额外要求 空间复杂度,那么再去思考版本二,因为版本二还是有点绕。版本一才是正常思路。