不同路径
题目描述
一个机器人位于一个 m x n
网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
示例 1:
输入:m = 3, n = 7
输出:28
示例 2:
输入:m = 3, n = 2
输出:3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。
1. 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右
3. 向下 -> 向右 -> 向下
示例 3:
输入:m = 7, n = 3
输出:28
示例 4:
输入:m = 3, n = 3
输出:6
解题思路
-
确定dp数组以及下标的含义
- dp[i][j] 表示到达从起始位置出发到达第(i,j)个坐标共有多少条路径。
-
确定递推公式
- 根据题意可知机器人只能向右或者向下运动,如果机器人想要运动到dp[i][j]位置,只有两种途径:1、从dp[i-1][j]向下运动 2、从dp[i][j-1]向右运动,因此可以得到递推公式:
dp[i][j] = dp[i-1][j] + dp[i][j-1]
- 根据题意可知机器人只能向右或者向下运动,如果机器人想要运动到dp[i][j]位置,只有两种途径:1、从dp[i-1][j]向下运动 2、从dp[i][j-1]向右运动,因此可以得到递推公式:
-
dp数组初始化
- 由于到达每个坐标的路径都能够由到达该坐标左侧和到达该坐标上方的路径和推出,那么在初始化的时候就需要对第一列和第一行的路径做初始化了,由于机器人只能向右或者向下,那么第一行和第一列的每个坐标都只有一种方式到达,即
dp[i][0] = 1
,dp[0][j] = 1
- 由于到达每个坐标的路径都能够由到达该坐标左侧和到达该坐标上方的路径和推出,那么在初始化的时候就需要对第一列和第一行的路径做初始化了,由于机器人只能向右或者向下,那么第一行和第一列的每个坐标都只有一种方式到达,即
-
确定遍历顺序
- 根据递推公式,
dp[i][j]
依赖于dp[i-1][j]
和dp[i][j-1]
,因此遍历方向为从上到下,从左往右
- 根据递推公式,
-
举例推导dp数组
-
假设 m = 3,n = 7,手动模拟dp[m][n]的推导数值如下:
-
代码实现
测试地址:https://leetcode.cn/problems/unique-paths/
class Solution {
public:
int uniquePaths(int m, int n) {
// 初始化一个 m 行 n 列的二维动态规划数组,所有元素初值为 0
vector<vector<int>> dp(m, vector<int>(n, 0));
// 将第一列的所有元素设置为 1,因为从起点到达任何第一列的位置都只有一条路径(一直向下)
for (int i = 0; i < m; i++)
dp[i][0] = 1;
// 将第一行的所有元素设置为 1,因为从起点到达任何第一行的位置都只有一条路径(一直向右)
for (int j = 0; j < n; j++)
dp[0][j] = 1;
// 从第二行和第二列开始,计算到达每个元素的路径数量
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
// 到达当前单元格的路径数是从左边单元格和上边单元格到达的路径数之和
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
// 返回到达最右下角的路径数
return dp[m - 1][n - 1];
}
};
不同路径 II
题目描述
一个机器人位于一个 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
解题思路
-
确定dp数组以及下标的含义
- dp[i][j] 表示到达从起始位置出发到达第(i,j)个坐标共有多少条路径。
-
确定递推公式
- 根据题意可知机器人只能向右或者向下运动,如果机器人想要运动到dp[i][j]位置,只有两种途径:1、从dp[i-1][j]向下运动 2、从dp[i][j-1]向右运动,本题和不同路径的差别是存在障碍物,再进行递推前需要先判断dp[i][j]是否存在障碍物,如果是的话就不对本条路径上的点进行赋值操作,递推公式为:
dp[i][j] = dp[i-1][j] + dp[i][j-1]
- 根据题意可知机器人只能向右或者向下运动,如果机器人想要运动到dp[i][j]位置,只有两种途径:1、从dp[i-1][j]向下运动 2、从dp[i][j-1]向右运动,本题和不同路径的差别是存在障碍物,再进行递推前需要先判断dp[i][j]是否存在障碍物,如果是的话就不对本条路径上的点进行赋值操作,递推公式为:
-
dp数组初始化
- 对第一行和第一列的坐标进行赋初值为1,当遇到障碍物时,说明后面的路已经走不通了,不赋值即可。
-
确定遍历顺序
- 根据递推公式,
dp[i][j]
依赖于dp[i-1][j]
和dp[i][j-1]
,遍历顺序为从上到下,从左到右
- 根据递推公式,
-
举例推导dp数组
-
以示例1为例,手动模拟dp[i][j]的递推过程:
-
代码实现
测试地址:https://leetcode.cn/problems/unique-paths-ii/
整数拆分
题目描述
给定一个正整数 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数组以及下标的含义
- dp[i]表示整数i经过拆分后得到的最大乘积
-
确定递推公式
-
对于每一个数i,我们都尝试将其分解为两个数j和(i-j),并计算乘积的最大值,当前数i的最大乘积可以有两种分解方式得到:
- 将i分解为j和(i - j),其乘积为(i - j) * j
- 将i分解为j和dp[i - j],由于i - j可以继续分解,所以我们考虑dp[i - j] * j
-
我们选取这两种分解方式中乘积更大的那一种,与当前dp[i]比较,取较大值,因此可以得到递推公式:
dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j))
-
-
dp数组初始化
- 因为dp[0]和dp[1]无法拆分的意义,因此只需要将dp[2]初始化为1即可。
-
确定遍历顺序
- dp[i] 是依靠 dp[i - j]的状态,所以遍历i一定是从前向后遍历,先有dp[i - j]再有dp[i]。
-
举例推导dp数组
-
当n=10的时候,手动模拟dp数组如下:
-
代码实现
测试地址:https://leetcode.cn/problems/integer-break/
class Solution {
public:
int integerBreak(int n) {
// 初始化动态规划表,长度为n+1,dp[i]表示将正整数i分解后的最大乘积
vector<int> dp(n + 1);
// 将2分解为两个整数之和的最大乘积是1
dp[2] = 1;
// 从3开始,迭代地计算每一个数i的分解乘积的最大值
for (int i = 3; i <= n ; i++) {
// 注意j只需要遍历到i/2即可,因为乘积与因子顺序无关
for (int j = 1; j <= i / 2; j++) {
//计算乘积的最大值
dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j));
}
}
// 返回将n分解后的最大乘积
return dp[n];
}
};
不同的二叉搜索树
题目描述
给你一个整数 n
,求恰由 n
个节点组成且节点值从 1
到 n
互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。
示例 1:
输入:n = 3
输出:5
示例 2:
输入:n = 1
输出:1
解题思路
-
确定dp数组以及下标的含义
- dp[i]表示由i个节点组合,一共能够得到dp[i]种不同的二叉搜索树
-
确定递推公式
-
根据二叉搜索树的定义可知,空节点也属于二叉搜索树,因此可以得出dp[0]为1,如下图所示:以1为头结点的二叉搜索搜索树只有一种可能,以2为头结点有两种可能,以3为头结点共有三种可能,其中当n=2的时候,我们可以拆分为两种情况:1、左子树为一个节点,右子树为空节点 2、左子树为空节点,右子树为1个节点。那么
dp[2] = dp[1] * dp[0] + dp[0] * dp[1]
,同理:dp[3] = dp[0] * dp[2] + dp[1] * dp[1] + dp[2] * d[0]
-
为什么使用的是
*
和不是+
呢,以n=3为例,左子树为2的时候,其实是有两种表示形态的,因此如果右子树也是2的时候,实际上会有2 * 2 = 4种排列组合情况。 -
这里我们来观察一下
dp[3] = dp[0] * dp[2] + dp[1] * dp[1] + dp[2] * d[0]
,实际上这个表达式可以看做:以1为头结点的时能够得到二叉树的种数 + 以2为头结点的时能够得到二叉树的种数 + 以3为头结点的时能够得到二叉树的种数,假设我们一共有i个节点,那么以j为头结点,那么一共会有j-1
个节点作为二叉搜索树的左子树,一共有i-j
个节点作为二叉搜索树的右子树,依次遍历j,就可以得到i个节点时所有的种树了,这里我们就可以得到递推表达式:d[i] = d[j-1] * d[i-j]
-
-
dp数组初始化
- 从上面的递推表达式可以得知,我们只需要将dp[0]进行初始化,往后所有的dp[i]均能通过递归表达式得到
-
确定遍历顺序
- 从dp[3]的递推结果可以看出,dp[3]的值和dp[0]、dp[1]、dp[2]均有关系,因此遍历顺序是从小到大的
-
举例推导dp数组
-
当 n = 5的时候,dp数组状态如下图所示:
-
代码实现
测试地址:https://leetcode.cn/problems/unique-binary-search-trees/
class Solution {
public:
// 计算由1到n的整数能构成的唯一二叉搜索树的数量
int numTrees(int n) {
// dp数组用于存储不同节点数量的唯一二叉搜索树数
// dp[i]表示i个节点能形成的二叉搜索树的数量
vector<int> dp(n + 1);
// 空树也是一种形态,所以初始化dp[0]为1
dp[0] = 1;
// 从1到n计算每个节点数下的唯一二叉搜索树数
for (int i = 1; i <= n; i++) {
// j遍历到i,将每个数作为根节点,左子树由j-1个节点构成,右子树由i-j个节点构成
for (int j = 1; j <= i; j++) {
// dp[i]累加以j为根节点时的二叉搜索树数量
// dp[j-1] * dp[i-j]:左子树的排列方式数 * 右子树的排列方式数
dp[i] += dp[j - 1] * dp[i - j];
}
}
// 返回n个节点构成的二叉搜索树的数量
return dp[n];
}
};