文章目录
-
- [Day 37](#Day 37)
-
- [00. 动态规划理论基础](#00. 动态规划理论基础)
- [01. 斐波那契数(No. 509)](#01. 斐波那契数(No. 509))
-
- [<1> 题目](#<1> 题目)
- [<2> 笔记](#<2> 笔记)
- [<3> 代码](#<3> 代码)
- [02. 爬楼梯(No. 70)](#02. 爬楼梯(No. 70))
-
- [<1> 题目](#<1> 题目)
- [<2> 笔记](#<2> 笔记)
- [<3> 代码](#<3> 代码)
- [03. 使用最小花费爬楼梯(No. 746)](#03. 使用最小花费爬楼梯(No. 746))
-
- [<1> 题目](#<1> 题目)
- [<2> 笔记](#<2> 笔记)
- [<3> 代码](#<3> 代码)
Day 37
00. 动态规划理论基础
最常见的动态规划题目其实就是 求最值,比如说股票问题、背包问题,都是在求使用怎样的策略能使得整个系统达到一个最优化的状态。
这是否和贪心比较类似呢?
其实贪心算法和动态规划算法的区别还是比较大的,贪心算法每一次的最优解一定 包含 上一次的最优解,是局部的最优推出全局的最优,而动态规划的最优解不一定包含前一次的最优解,而是有可能是由更前面的部分推出的,所以通常通过 dp[]
数组来将前面的所有最优解来保存下来。
动态规划其实是一个 穷举 的过程,得到最优解的前提就是要将所有的可能导致最优解的情况列出来,逐步推出最终的结果,而贪心更像是确定了一个路线,直接来走这个最优的路线,但这种最优通常是一种经验性的,较难推导的方式,相信做过贪心部分的朋友应该深有体会,这也就导致贪心得到的可能不是最优解,但相对的时间复杂度较低,而动态规划本质是穷举就会导致其时间复杂度相对较高。
再来谈谈动态规划和暴力解法的区别,比如说斐波那契数列,使用递归来求会导致大量的重复计算,所以考虑引入备忘录,也就是记忆搜索的方法,记忆搜索的方法是自顶向下的,也就是要算 f(5)
要递归到 f(1)
才开始计算结果,而且由于递归的限制,思考其实可以采用自底向下的方式,从 f(1)
开始向上层递归,这其实就是动态规划的方法。
01. 斐波那契数(No. 509)
<1> 题目
斐波那契数 (通常用 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
<2> 笔记
这道题目相信大家都能比较容易的写出来,这里以本题为案例看看动态规划的一些小特点。
动态规划比较重要的一个点就是 状态 ,解题的关键在于能否列出正确的 状态转移方程 ,以及这个状态转移方程最终能否穷举出问题的最终答案,其实这就要求这个算法问题具备 最优子结构 ;因为需要不断用到前面的状态来推导后面状态的最优,这就会引出大量的重复的计算这也就是常说的 重叠子问题,所以使用暴力解法的效率很低。
这道斐波那契数列其实就很好的展示了重叠子问题,但其实并不存在重叠子问题,所以严格上并不算动态规划的题目。
比如计算 f(5)
,画出递归树
可以看出其实是计算了两遍 f(3)
的,如果说计算的数字更大,子问题的个数会以指数级的速度增长,时间复杂度为 2^n^。这就是所谓的重叠子问题。
可以注意到 f(n)
的状态其实是由 f(n - 1)
和 f(n - 2)
推导出来的,转移的方式其实就是两者加和,这样其实就很容易看出 dp
数组的含义,就是 dp[n] = f(n)
,状态转移公式也顺带推了出来(这就是本题较为简单的原因),因为这几步几乎不用怎么思考。
直接写出代码。
<3> 代码
java
class Solution {
public int fib(int n) {
if (n == 0) return 0;
if (n <= 2) return 1;
int[] dp = new int[n + 1];
dp[1] = 1;
dp[2] = 1;
for (int i = 3; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
}
02. 爬楼梯(No. 70)
<1> 题目
假设你正在爬楼梯。需要 n
阶你才能到达楼顶。
每次你可以爬 1
或 2
个台阶。你有多少种不同的方法可以爬到楼顶呢?
示例 1:
输入:n = 2
输出:2
解释:有两种方法可以爬到楼顶。
- 1 阶 + 1 阶
- 2 阶
示例 2:
输入:n = 3
输出:3
解释:有三种方法可以爬到楼顶。
- 1 阶 + 1 阶 + 1 阶
- 1 阶 + 2 阶
- 2 阶 + 1 阶
提示:
1 <= n <= 45
<2> 笔记
本题其实也没有引入太多的递归思想,其实可以看作是下一题 使用最小花费爬楼梯 的一个导入。
本题的 dp
定义其实是较为简单的,因为要得出 n
阶台阶由几种走法,可以想到就是用 dp[n]
来代表共 n
阶由多少种走法。
爬到第一层楼有一种方法,爬到第二层有两种方法,那爬到第三层其实就有三种方法,一种是通过第一层跨两步,另一种是第二层跨一步。
很多朋友做这道题的时候会有一个疑问的地方,就是我从第一层走一步再走一步算不算一种方法呢?或者说从第四层走两次到第五层,这里想不通的朋友是因为没有将目光放到全局,其实将这些方式写出来得到的数列是相同的,比如 n = 3
的时候 1 1 1,可以理解成从 2 到 3 的一部分,也可以理解成从 1 跨两步到 3,计算的话其实就计算其中的一部分即可,否则会出现冲突。
使用递归五步法来规范一下:
- 确认
dp
的含义:爬到第i层楼梯,有dp[i]种方法 - 确认递推公式:dp[i] = dp[i - 1] + dp[i - 2]
dp
数组如何初始化:题目中说了n是一个正整数,所以dp[0]
是没有意义的,而递归推出后面的部分有需要两个元素,所以初始化就初始化dp[1]
和dp[2]
- 递归的遍历顺序:需要用到前面的元素,从前向后遍历
- 尝试举例推导
<3> 代码
java
class Solution {
public int climbStairs(int n) {
if (n < 3) return n;
int[] dp = new int[n + 1];
dp[1] = 1;
dp[2] = 2;
for (int i = 3; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
}
03. 使用最小花费爬楼梯(No. 746)
<1> 题目
给你一个整数数组 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
<2> 笔记
有了上一道题的铺垫其实本题就比较容易了,到达台阶 n
有两种方式分别是从 n - 1
和 n - 2
到达,所以需要有一个数组来存储之前的状态。
- 确定dp数组以及下标的含义,使用动态规划,就要有一个数组来记录状态,本题只需要一个一维数组dp[i]就可以了,dp[i]的定义:到达第i台阶所花费的最少体力为dp[i]。
- 确定递推公式:dp[i - 1] 跳到 dp[i] 需要花费 dp[i - 1] + cost[i - 1],dp[i - 2] 跳到 dp[i] 需要花费 dp[i - 2] + cost[i - 2],那选择哪个呢?一定是最小的,所以 dp[i] = Math.min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
- dp 数组如何初始化:和上面题目相同,只需要初始化两个即可,也就是
dp[0]
和dp[1]
,分别初始化成cost[0]
和ocst[1]
- 确定遍历顺序:和上题相同,都是自底向上,从前向后遍历
- 举例推导,发现没有问题
通过上面的梳理就能写出代码
<3> 代码
java
class Solution {
public int minCostClimbingStairs(int[] cost) {
int n = cost.length;
if (n < 2) {
return 0;
}
int[] dp = new int[n + 1];
dp[0] = 0;
dp[1] = 0;
for (int i = 2; i <= n; i++) {
dp[i] = Math.min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
}
return dp[n];
}
}