Day 28
理论基础
无论大家之前对动态规划学到什么程度,一定要先看 我讲的 动态规划理论基础。
如果没做过动态规划的题目,看我讲的理论基础,会有感觉 是不是简单题想复杂了?
其实并没有,我讲的理论基础内容,在动规章节所有题目都有运用,所以很重要!
如果做过动态规划题目的录友,看我的理论基础 就会感同身受了。
文章讲解:https://programmercarl.com/动态规划理论基础.html
视频讲解:https://www.bilibili.com/video/BV13Q4y197Wg
基本介绍
动态规划,英文:Dynamic Programming,简称 DP,如果某一问题有很多重叠子问题,使用动态规划是最有效的。
所以动态规划中每一个状态一定是由上一个状态推导出来的,这一点就区分于贪心,贪心没有状态推导,而是从局部直接选最优的,
动态规划框架⭐
(1)确定 dp 数组(dp table)以及下标的含义
(2)确定递推公式
(3)dp 数组如何初始化
(4)确定遍历顺序
(5)举例推导 dp 数组
509. 斐波那契数
很简单的动规入门题,但简单题使用来掌握方法论的,还是要有动规五部曲来分析。
题目链接:https://leetcode.cn/problems/fibonacci-number/description
文章讲解:https://programmercarl.com/0509.斐波那契数.html
视频讲解:https://www.bilibili.com/video/BV1f5411K7mo
思路分析
(1)确定 dp 数组以及下标的含义
dp[i]的定义为:第 i 个数的斐波那契数值是 dp[i]
(2)确定递推公式
题目已经把递推公式直接给我们了:状态转移方程 dp[i] = dp[i - 1] + dp[i - 2];
(3)dp 数组如何初始化
题目中给出如下定义
dp[0] = 0;
dp[1] = 1;(4)确定遍历顺序
从递归公式 dp[i] = dp[i - 1] + dp[i - 2]中可以看出,dp[i] 是依赖 dp[i - 1] 和 dp[i - 2],那么遍历的顺序一定是从前向后遍历的
(5)举例推导 dp 数组
按照这个递推公式 dp[i] = dp[i - 1] + dp[i - 2],我们来推导一下,当 N 为 10 的时候,dp 数组应该是如下的数列
0、1、1、2、3、5、8、13、21、34、55
提示:如果代码写出来,发现结果不对,就把 dp 数组打印出来看看和我们推导的数列是不是一致的。
题解
class Solution {
public int fib(int n) {
if (n <= 1) {
return n;
}
int[] dp = new int[n + 1];
// 第 0 个斐波那契数
dp[0] = 0;
// 第 1 个斐波那契数
dp[1] = 1;
// 要求第 n 个斐波那契数,遍历到 n
for (int index = 2; index <= n; index++) {
dp[index] = dp[index - 1] + dp[index - 2];
}
return dp[n];
}
}状态压缩
根据递推公式可以发现,dp[i] 只依赖于前两个值,可以不用维护整个数组,只需要维护 dp[i]、dp[i - 1]、dp[i - 2]这三个值即可

class Solution {
public int fib(int n) {
if (n <= 1) {
return n;
}
int[] dp = new int[2];
// 第 0 个斐波那契数
dp[0] = 0;
// 第 1 个斐波那契数
dp[1] = 1;
/*
(1)要求第 n 个斐波那契数,遍历到 n
(2)状态压缩,只需要维护三个变量(i - 2、i - 1、i)
*/
for (int index = 2; index <= n; index++) {
// dp[i],第 i 个斐波那契数的值
int sum = dp[0] + dp[1];
// 下一轮循环,不断更新
dp[0] = dp[1];
dp[1] = sum;
}
// 经过不断更新,最终 sum 的值保存在了 dp[1] 中
return dp[1];
}
}70. 爬楼梯
本题大家先自己想一想, 之后会发现,和斐波那契数有点关系。
题目链接:https://leetcode.cn/problems/climbing-stairs
文章讲解:https://programmercarl.com/0070.爬楼梯.html
视频讲解:https://www.bilibili.com/video/BV17h411h7UH
思路分析
(1)确定 dp 数组以及下标的含义
dp[i]: 爬到第 i 层楼梯,有 dp[i] 种方法
(2)确定递推公式
题目中提到,每次只能走一步或者两步,也就是说方法依赖于前两个台阶的方法数
dp[i - 1],上 i-1 层楼梯,有 dp[i - 1]种方法,那么再一步跳一个台阶不就是 dp[i]了么。
dp[i - 2],上 i-2 层楼梯,有 dp[i - 2]种方法,那么再一步跳两个台阶不就是 dp[i]了么。
dp[i] = dp[i - 1] + dp[i - 2]
最后发现,本质就是斐波那契数列
(3)dp 数组如何初始化
题目中说了 n 是一个正整数,题目根本就没说 n 有为 0 的情况,不需要讨论 dp[0]的初始化
从 i = 3 开始递推,这样才符合 dp[i] 的定义
dp[1] = 1;
dp[2] = 2;(4)确定遍历顺序
从递推公式 dp[i] = dp[i - 1] + dp[i - 2]中可以看出,遍历顺序一定是从前向后遍历的
(5)举例推导 dp 数组

题解
class Solution {
public int climbStairs(int n) {
// 防止下标索引越界,同时处理特殊情况
if (n <= 1){
return n;
}
// dp[0] 没有意义,从dp[1]开始,正好对应第 i 个台阶
int[] dp = new int[n + 1];
dp[1] = 1;
dp[2] = 2;
// 从 3 开始递推
for (int i = 3; i <= n; i++) {
// dp[i - 1]:走一步
// dp[i - 2]:走两步
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
}状态压缩
维护的仍然是三个数,不需要维护整个数组,再空间上优化
class Solution {
public int climbStairs(int n) {
// 处理特殊情况
if (n <= 1) {
return n;
}
// dp[0] 没有意义,从dp[1]开始,正好对应第 i 个台阶
int[] dp = new int[3];
dp[1] = 1;
dp[2] = 2;
// 从 3 开始递推
for (int i = 3; i <= n; i++) {
int sum = dp[1] + dp[2];
dp[1] = dp[2];
dp[2] = sum;
}
return dp[2];
}
}746. 使用最小花费爬楼梯
这道题目力扣改了题目描述了,现在的题目描述清晰很多,相当于明确说 第一步是不用花费的。
更改题目描述之后,相当于是 文章中 「拓展」的解法
题目链接:https://leetcode.cn/problems/min-cost-climbing-stairs
文章讲解:https://programmercarl.com/0746.使用最小花费爬楼梯.html
视频讲解:https://www.bilibili.com/video/BV16G411c7yZ
思路分析
(1)确定 dp 数组以及下标的含义
dp[i]的定义:到达第 i 台阶的最少花费为 dp[i]
(2)确定递推公式
可以有两个途径得到 dp[i],一个是 dp[i-1] 一个是 dp[i-2]
dp[i - 1] 跳到 dp[i] 需要花费 dp[i - 1] + cost[i - 1]
解释:到达 dp[i - 1] 台阶的最小花费 + 跳到下一个台阶的花费 cost[i - 1]
dp[i - 2] 跳到 dp[i] 需要花费 dp[i - 2] + cost[i - 2]
题目要求花费最少,则 dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2])
(3)dp 数组如何初始化
题目描述中明确说了<<你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯>>
也就是说到达第 0 个台阶是不花费的,但从第 0 个台阶往上跳的话,需要花费 cost[0] 初始化 dp[0] = 0,dp[1] = 0
(4)确定遍历顺序
因为是模拟台阶,而且 dp[i]由 dp[i-1]dp[i-2]推出,所以是从前到后遍历 cost 数组就可以了
(5)举例推导 dp 数组

题解
class Solution {
public int minCostClimbingStairs(int[] cost) {
/*
(1)dp[i] 表示到达第 i 个台阶的最小花费
(2)cost[cost.length - 1] 表示跳到顶楼的花费,
还要再朓一步才到达顶楼的位置,
(3)根据 dp[] 数组的含义,返回的应该是到达顶楼的最小花费,
即初始化时数组的长度要加一,加一到达的位置就是顶楼的位置
*/
int[] dp = new int[cost.length + 1];
// 从下标为 0 或者 下标为 1 的地方开始朓,跳了才有花费
dp[0] = 0;
dp[1] = 0;
for (int i = 2; i <= cost.length; i++) {
// 取最小花费
dp[i] = Math.min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
}
// 返回达到顶楼的最小花费
return dp[cost.length];
}
}状态压缩
class Solution {
public int minCostClimbingStairs(int[] cost) {
// 状态压缩,dp[i]就是由前两位推出来的
int dp0 = 0;
int dp1 = 0;
for (int i = 2; i <= cost.length; i++) {
int dpi = Math.min(dp1 + cost[i - 1], dp0 + cost[i - 2]);
// 下一轮循环,更新
dp0 = dp1;
dp1 = dpi;
}
return dp1;
}
}