代码随想录训练营 Day34打卡 动态规划 part02
一、力扣62. 不同路径
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
示例:
输入:m = 3, n = 7
输出:28
想要求dp[i][j],只能有两个方向来推导出来,即dp[i - 1][j] 和 dp[i][j - 1]。
此时在回顾一下 dp[i - 1][j] 表示啥,是从(0, 0)的位置到(i - 1, j)有几条路径,dp[i][j - 1]同理。
那么很自然,dp[i][j] = dp[i - 1][j] + dp[i][j - 1],因为dp[i][j]只有这两个方向过来。
如何初始化呢,首先dp[i][0]一定都是1,因为从(0, 0)的位置到(i, 0)的路径只有一条,那么dp[0][j]也同理。
所以初始化代码为:
for (int i = 0; i < m; i++) dp[i][0] = 1;
for (int j = 0; j < n; j++) dp[0][j] = 1;
举例推导:
代码实现
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
# 创建一个 m x n 的二维列表 dp,其中 dp[i][j] 表示到达单元格 (i, j) 的唯一路径数
dp = [[0] * n for _ in range(m)]
# 初始化第一列:从起点 (0, 0) 到任意单元格 (i, 0) 的路径数都为 1,
# 因为只能一直往下走
for i in range(m):
dp[i][0] = 1
# 初始化第一行:从起点 (0, 0) 到任意单元格 (0, j) 的路径数都为 1,
# 因为只能一直往右走
for j in range(n):
dp[0][j] = 1
# 遍历剩余的单元格,计算每个单元格的唯一路径数
# dp[i][j] 的值等于从上方单元格 (i-1, j) 和左侧单元格 (i, j-1) 到达的路径数之和
for i in range(1, m):
for j in range(1, n):
dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
# 返回右下角单元格 (m-1, n-1) 的唯一路径数
return dp[m - 1][n - 1]
力扣题目链接
题目文章讲解
题目视频讲解
二、 力扣63. 不同路径 II
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish”)。
现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
网格中的障碍物和空位置分别用 1 和 0 来表示。
示例:
输入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]]
输出:2
解释:3x3 网格的正中间有一个障碍物。
从左上角到右下角一共有 2 条不同的路径:
1.向右 -> 向右 -> 向下 -> 向下
2.向下 -> 向下 -> 向右 -> 向右
递推公式和62.不同路径一样,dp[i][j] = dp[i - 1][j] + dp[i][j - 1]。
但这里需要注意一点,因为有了障碍,(i, j)如果就是障碍的话应该就保持初始状态(初始状态为0)。
因为从(0, 0)的位置到(i, 0)的路径只有一条,所以dp[i][0]一定为1,dp[0][j]也同理。
但如果(i, 0) 这条边有了障碍之后,障碍之后(包括障碍)都是走不到的位置了,所以障碍之后的dp[i][0]应该还是初始值0。
如图:
下标(0, j)的初始化情况同理。
举例如题:
对应的dp table 如图:
代码实现
class Solution:
def uniquePathsWithObstacles(self, obstacleGrid):
# 获取网格的行数和列数
m = len(obstacleGrid)
n = len(obstacleGrid[0])
# 如果起点 (0, 0) 或终点 (m-1, n-1) 处有障碍物,直接返回 0,因为无法到达终点
if obstacleGrid[m - 1][n - 1] == 1 or obstacleGrid[0][0] == 1:
return 0
# 创建一个 m x n 的二维列表 dp,用于存储到达每个单元格的唯一路径数
dp = [[0] * n for _ in range(m)]
# 初始化第一列
# 如果某个单元格 (i, 0) 没有障碍物,则 dp[i][0] = 1(表示从起点到这个单元格有一条路径)
# 如果某个单元格 (i, 0) 有障碍物,则后面的所有单元格都无法到达,因此直接退出循环
for i in range(m):
if obstacleGrid[i][0] == 0: # 当前单元格没有障碍物
dp[i][0] = 1
else: # 当前单元格有障碍物,后续单元格不可达
break
# 初始化第一行
# 类似地,如果某个单元格 (0, j) 没有障碍物,则 dp[0][j] = 1
# 如果某个单元格 (0, j) 有障碍物,则后面的所有单元格都无法到达,因此直接退出循环
for j in range(n):
if obstacleGrid[0][j] == 0: # 当前单元格没有障碍物
dp[0][j] = 1
else: # 当前单元格有障碍物,后续单元格不可达
break
# 计算剩余网格中的每个单元格的唯一路径数
for i in range(1, m):
for j in range(1, n):
# 如果当前单元格 (i, j) 有障碍物,路径数保持为 0,跳过该单元格
if obstacleGrid[i][j] == 1:
continue
# 如果没有障碍物,当前单元格的路径数等于从上方单元格和左侧单元格到达的路径数之和
dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
# 返回右下角单元格 (m-1, n-1) 的路径数,即为最终答案
return dp[m - 1][n - 1]
力扣题目链接
题目文章讲解
题目视频讲解
三、 力扣343. 整数拆分
给定一个正整数 n ,将其拆分为 k 个 正整数 的和( k >= 2 ),并使这些整数的乘积最大化。
返回 你可以获得的最大乘积 。
示例:
输入: n = 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1。
dp[i]:分拆数字i,可以得到的最大乘积为dp[i]。
dp[i]最大乘积是怎么得到的呢?
一个是j * (i - j) 直接相乘。
一个是j * dp[i - j],相当于是拆分(i - j),对这个拆分不理解的话,可以回想dp数组的定义。
举例当n为10 的时候,dp数组里的数值,如下:
代码实现
class Solution:
# 假设对正整数 i 拆分出的第一个正整数是 j(1 <= j < i),则有以下两种方案:
# 1) 将 i 拆分成 j 和 i−j 的和,且 i−j 不再拆分成多个正整数,此时的乘积是 j * (i-j)
# 2) 将 i 拆分成 j 和 i−j 的和,且 i−j 继续拆分成多个正整数,此时的乘积是 j * dp[i-j]
def integerBreak(self, n):
# 创建一个大小为 n+1 的数组 dp,用于存储从 1 到 n 的每个数字的最大乘积
dp = [0] * (n + 1)
# 初始化 dp[2] 为 1,因为当 n=2 时,只有一种拆分方式:1+1=2,乘积为 1
dp[2] = 1
# 从 3 开始计算,直到 n
for i in range(3, n + 1):
# 遍历所有可能的第一个拆分点 j (1 <= j < i),j 最大不超过 i 的一半
for j in range(1, i // 2 + 1):
# 对于每个拆分点 j,有以下三种情况需要比较:
# 1) 直接将 i 拆分成 j 和 i-j 的和,乘积为 j * (i-j)
# 2) 将 i 拆分成 j 和 i-j 的和,并将 i-j 继续拆分成多个正整数,乘积为 j * dp[i-j]
# 3) 之前计算得到的 dp[i](即尚未考虑当前拆分点 j 的最大乘积)
dp[i] = max(dp[i], (i - j) * j, dp[i - j] * j)
# 返回 dp[n],即将 n 拆分为多个正整数的最大乘积
return dp[n]
力扣题目链接
题目文章讲解
题目视频讲解
四、 力扣96. 不同的二叉搜索树
给你一个整数 n ,求恰由 n 个节点组成且节点值从 1 到 n 互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。
示例:
输入:n = 3
输出:5
我们应该先举几个例子,画画图,看看有没有什么规律,如图:
元素3为头结点搜索树的数量 = 右子树有0个元素的搜索树数量 * 左子树有2个元素的搜索树数量
有2个元素的搜索树数量就是dp[2]。
有1个元素的搜索树数量就是dp[1]。
有0个元素的搜索树数量就是dp[0]。
所以dp[3] = dp[2] * dp[0] + dp[1] * dp[1] + dp[0] * dp[2]
如图所示:
dp[i] : 1到i为节点组成的二叉搜索树的个数为dp[i]。
在上面的分析中,其实已经看出其递推关系, dp[i] += dp[以j为头结点左子树节点数量] * dp[以j为头结点右子树节点数量]
j相当于是头结点的元素,从1遍历到i为止。
所以递推公式:dp[i] += dp[j - 1] * dp[i - j]; ,j-1 为j为头结点左子树节点数量,i-j 为以j为头结点右子树节点数量
n为5时候的dp数组状态如图:
代码实现
class Solution:
def numTrees(self, n: int) -> int:
# 创建一个长度为 n+1 的数组 dp,用于存储从 1 到 n 的每个数字组成的二叉搜索树的数量
dp = [0] * (n + 1)
# 当 n 为 0 时,只有一种情况,即空树,所以 dp[0] = 1
dp[0] = 1
# 外层循环:从 1 到 n,计算每个 i 对应的二叉搜索树的数量
for i in range(1, n + 1):
# 内层循环:对于每个 i,选择每个 j 作为根节点,计算不同二叉搜索树的数量
for j in range(1, i + 1):
# 递推公式:dp[i] += dp[j - 1] * dp[i - j]
# dp[j - 1] 表示以 j 为根节点的左子树的节点数量的组合情况
# dp[i - j] 表示以 j 为根节点的右子树的节点数量的组合情况
dp[i] += dp[j - 1] * dp[i - j]
# 返回以 1 到 n 为节点的二叉搜索树的总数量
return dp[n]
力扣题目链接
题目文章讲解
题目视频讲解