文章目录

Day 3700. 动态规划理论基础01. 斐波那契数(No. 509)<1> 题目<2> 笔记<3> 代码

02. 爬楼梯(No. 70)<1> 题目<2> 笔记<3> 代码

03. 使用最小花费爬楼梯(No. 746)<1> 题目<2> 笔记<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) 的,如果说计算的数字更大,子问题的个数会以指数级的速度增长,时间复杂度为 2n。这就是所谓的重叠子问题。

可以注意到 f(n) 的状态其实是由 f(n - 1) 和 f(n - 2) 推导出来的,转移的方式其实就是两者加和,这样其实就很容易看出 dp 数组的含义,就是 dp[n] = f(n),状态转移公式也顺带推了出来(这就是本题较为简单的原因),因为这几步几乎不用怎么思考。

直接写出代码。

<3> 代码

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> 代码

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 <= 10000 <= 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> 代码

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];

}

}

精彩链接

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: