一、使用最小花费爬楼梯
题目:
给你一个整数数组 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 。
思路:
这里定义dp[i]数组的含义是爬到第i层所使用的最小花费为dp[i],而cost[i]的含义是从当前第i层往上爬的花费为cost[i],因此可得递推式为dp[i]=min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2])
代码:
public int minCostClimbingStairs(int[] cost) {
// 创建一个 dp 数组,长度为 cost.length + 1,用于存储每一级的最小花费
int[] dp = new int[cost.length + 1];
// 初始化 dp 数组的前两项
dp[0] = 0; // 第 0 级的最小花费为 0
dp[1] = 0; // 第 1 级的最小花费为 0
// 从第 2 级开始计算到达每一级的最小花费
for (int i = 2; i <= cost.length; i++) {
// dp[i] 表示到达第 i 级的最小花费
// 要么从第 i-1 级到达第 i 级,需要花费 dp[i-1] + cost[i-1]
// 要么从第 i-2 级到达第 i 级,需要花费 dp[i-2] + cost[i-2]
// 取这两者中的最小值
dp[i] = Math.min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
}
// 返回到达楼梯顶端的最小花费,即 dp[cost.length]
return dp[cost.length];
}
-
创建 dp 数组:
int[] dp = new int[cost.length + 1];
:创建一个dp
数组,用于记录到达每一级的最小花费。数组的长度是cost.length + 1
,因为我们还需要考虑到达终点的情况。
-
初始化 dp 数组的前两项:
dp[0] = 0;
:起点到达第 0 级的最小花费是 0。dp[1] = 0;
:起点到达第 1 级的最小花费是 0。尽管不需要使用cost[0]
和cost[1]
的实际值来进行计算,但在这个问题中,通常这两个初始值设为 0 是为了处理边界条件。
-
计算每一级的最小花费:
for (int i = 2; i <= cost.length; i++) {...}
:从第 2 级开始计算,直到到达最后一层楼梯(即cost.length
级)。dp[i] = Math.min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
:对于每一级i
,选择从前一级i-1
还是前两级i-2
中的最小花费加上相应的cost
。
-
返回结果:
return dp[cost.length];
:返回到达楼梯顶端的最小花费,即dp
数组的最后一个元素。
二、不同路径2
题目:
一个机器人位于一个 m x n
网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish”)。
现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
网格中的障碍物和空位置分别用 1
和 0
来表示。
示例 1:
输入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]] 输出:2 解释:3x3 网格的正中间有一个障碍物。 从左上角到右下角一共有2条不同的路径: 1. 向右 -> 向右 -> 向下 -> 向下 2. 向下 -> 向下 -> 向右 -> 向右
示例 2:
输入:obstacleGrid = [[0,1],[0,0]] 输出:1
思路:
与不同路径1不同的是,该题需要判断在行驶路中是否可达,如果不可到达直接返回,可达则继续遍历,判断到达终点有几条路径的前提是路上无障碍物
代码:
public int uniquePathsWithObstacles(int[][] obstacleGrid) {
int m = obstacleGrid.length; // 网格的行数
int n = obstacleGrid[0].length; // 网格的列数
int[][] dp = new int[m][n]; // 创建一个二维数组 dp,用于存储到达每个格子的路径数
// 如果起点或终点有障碍物,则无法到达终点
if (obstacleGrid[m - 1][n - 1] == 1 || obstacleGrid[0][0] == 1) {
return 0;
}
// 初始化第一列:如果第一列的某个格子没有障碍物,则该列的路径数全为 1
for (int i = 0; i < m && obstacleGrid[i][0] == 0; i++) {
dp[i][0] = 1;
}
// 初始化第一行:如果第一行的某个格子没有障碍物,则该行的路径数全为 1
for (int i = 0; i < n && obstacleGrid[0][i] == 0; i++) {
dp[0][i] = 1;
}
// 遍历剩下的格子,填充 dp 数组
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
// 如果当前格子没有障碍物,则路径数为从上方和左方的路径数之和
if (obstacleGrid[i][j] == 0) {
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
} else {
// 如果当前格子有障碍物,则路径数为 0
dp[i][j] = 0;
}
}
}
// 返回到达终点的路径数
return dp[m - 1][n - 1];
}
-
变量初始化:
m
和n
分别表示网格的行数和列数。dp
是一个二维数组,用于记录到达每个格子的路径数。
-
边界条件处理:
- 如果起点 (
obstacleGrid[0][0]
) 或终点 (obstacleGrid[m - 1][n - 1]
) 位置有障碍物,则返回 0,因为无法从起点到达终点。
- 如果起点 (
-
第一列和第一行初始化:
- 对于没有障碍物的第一列,路径数为 1,因为只能从上方到达。
- 对于没有障碍物的第一行,路径数为 1,因为只能从左方到达。
-
动态规划填充 dp 数组:
- 遍历网格中的每个格子,计算到达该格子的路径数。若当前位置无障碍物,路径数是从上方和左方格子的路径数之和。若有障碍物,路径数为 0。
-
结果返回:
- 最终返回到达终点
(m-1, n-1)
的路径数。
- 最终返回到达终点
三、整数拆分
题目:
给定一个正整数 n
,将其拆分为 k
个 正整数 的和( k >= 2
),并使这些整数的乘积最大化。
返回 你可以获得的最大乘积 。
示例 1:
输入: n = 2 输出: 1 解释: 2 = 1 + 1, 1 × 1 = 1。
示例 2:
输入: n = 10 输出: 36 解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。
思路:
定义dp[i]数组的含义为,拆分数字i得到的数的乘积为dp[i],拆分一次得到的数为j,则该次拆分的乘积为j*(i-j),而继续拆分(i-j)相乘得到的成绩则为(j*dp[i-j]),因此就应比较该次拆分与下一次拆分所得的最大值
或者说
j * (i - j) 是单纯的把整数拆分为两个数相乘
j * dp[i - j]是拆分成两个以及两个以上的个数相乘
可得递推式为: dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j))
代码:
public class Solution {
public int integerBreak(int n) {
// 创建一个大小为 n + 1 的数组,用于存储每个整数 i 的最大乘积
int[] dp = new int[n + 1];
// 基本情况:整数 2 的最大乘积是 1
dp[2] = 1;
// 从整数 3 到 n,计算每个整数 i 的最大乘积
for (int i = 3; i <= n; i++) {
// 遍历 j 从 1 到 i / 2,计算不同分解方式的最大乘积
for (int j = 1; j <= i / 2; j++) {
// 更新 dp[i] 为以下两者的最大值:
// 1. j * (i - j): 直接分解成 j 和 i - j
// 2. j * dp[i - j]: 将 i - j 进一步分解,然后乘以 j
dp[i] = Math.max(dp[i], Math.max(j * (i - j), j * dp[i - j]));
}
}
// 返回整数 n 的最大乘积
return dp[n];
}
}
dp
是一个大小为n + 1
的数组,用于存储每个整数值i
(2
到n
)可以分解的最大乘积。- 初始化
dp[2] = 1
,因为对于n = 2
,唯一的分解方式是1 + 1
,乘积为1
。 - 外层循环
i
遍历从3
到n
的所有整数。 - 内层循环
j
遍历从1
到i / 2
的所有值(i / 2
是为了避免重复计算,因为j
和i - j
是对称的)。 - 对于每个
i
和j
,计算以下两个值的最大值:j * (i - j)
:即j
和i - j
的乘积,表示将i
分解成j
和i - j
两部分时的乘积。j * dp[i - j]
:即j
乘以dp[i - j]
,表示将i - j
进一步分解并乘以j
时的乘积。
- 更新
dp[i]
为上述两个值的最大值,确保dp[i]
始终存储从i
分解得到的最大乘积。 - 最终返回
dp[n]
,即将整数n
分解成至少两个正整数的最大乘积。
四、不同的二叉搜索树
题目:
给你一个整数 n
,求恰由 n
个节点组成且节点值从 1
到 n
互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。
示例 1:
输入:n = 3 输出:5
示例 2:
输入:n = 1 输出:1
思路:
首先,dp数组的含义是 i 个节点可以组成的二叉搜索树有dp[i]种,关键在于递推公式的确立,我们以其中一个节点 j 为根节点,
以节点 j 为根节点的,满足二叉搜索树的特性,其左子树的节点均比 j 的值小,数量则为j-1个,右子树的节点均比 j 的值大,数量则为i-j个,得到第一步
i = j + ( i - j )
对于n=3的二叉搜索树来说:
如果头节点为1,则其可以组成的二叉搜索树的情况为 左子树0个节点×右子树2个节点(2,3)
如果头节点为2,则情况有 左子树1个节点(0)× 右子树1个节点(3)
如果头节点为3,则情况为 左子树2个节点(1,2)× 右子树0个节点
可知 i 元素中可组成的二叉搜索树总个数为,其左右子树可组成的二叉搜索树的情况相乘后的总和
因此,可得递推公式:
dp[i] += dp[j-1] * dp[i-j]
其中,dp[j-1] 为左子树中全部符合二叉搜索树的情况,dp[i-j] 为右子树中全部符合二叉搜索树的情况
接下俩对dp数组进行初始化,节点数为0和1时,显然,可组成的二叉搜索树只有一种,因此
dp[0] = 1; dp[1] = 1;
代码:
public class Solution {
public int numTrees(int n) {
// 创建一个大小为 n + 1 的数组,用于存储每个整数 i 对应的不同二叉搜索树的数量
int[] dp = new int[n + 1];
// 基本情况:0 个节点和 1 个节点的二叉搜索树数量都为 1
dp[0] = 1; // 空树
dp[1] = 1; // 只有一个节点的树
// 遍历每个节点数量 i,从 2 到 n
for (int i = 2; i <= n; i++) {
// 对于每个 i,尝试以 j 作为根节点
for (int j = 1; j <= i; j++) {
// 当以 j 作为根节点时,左子树有 j - 1 个节点,右子树有 i - j 个节点
// 将左子树和右子树的不同二叉搜索树的数量相乘,并累加到 dp[i] 中
dp[i] += dp[j - 1] * dp[i - j];
}
}
// 返回具有 n 个节点的二叉搜索树的总数量
return dp[n];
}
}
-
状态定义:
dp[i]
表示具有i
个节点的二叉搜索树的不同结构数量。
-
基本情况:
dp[0] = 1
:没有节点的树只有一种情况,即空树。dp[1] = 1
:只有一个节点的树也只有一种情况。
-
状态转移:
- 对于每个
i
节点的树,我们考虑每个节点j
作为根节点。 - 如果节点
j
是根节点,那么:- 左子树包含
j - 1
个节点。 - 右子树包含
i - j
个节点。
- 左子树包含
- 左子树和右子树的不同结构数量分别是
dp[j - 1]
和dp[i - j]
。 - 计算方法是将左子树和右子树的结构数量相乘,然后将所有可能的根节点
j
对应的结果累加起来。
- 对于每个
-
最终结果:
dp[n]
存储的是具有n
个节点的二叉搜索树的不同结构数量。
今天的学习就到这里