62. 不同路径
题目描述
一个机器人位于一个 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
提示:
1 <= m, n <= 100
- 题目数据保证答案小于等于
2 * 109
题解
贪心算法解决,注意 “每次只能向下或向右移动一步” ,这就给我们提供了重叠子问题和最优子结构的基础。
dp
数组含义:dp[i][j]
表示达到位置 ( i , j ) (i, \enspace j) (i,j) 的路径数量- 初始化:起点为
(
0
,
0
)
(0, \enspace 0)
(0,0) ,显然只有唯一方法到达起点,即
dp[0][0] = 1
- 状态转移方程:
- 边界情况
- 如果是上边界,要到达
(
i
,
j
)
(i, \enspace j)
(i,j) 显然只能通过左边的格子向右走一步到达,即
dp[i][j] = dp[i][j - 1]
- 如果是左边界,要到达
(
i
,
j
)
(i, \enspace j)
(i,j) 显然只能通过上面的格子向下走一步到达,即
dp[i][j] = dp[i - 1][j]
- 如果是上边界,要到达
(
i
,
j
)
(i, \enspace j)
(i,j) 显然只能通过左边的格子向右走一步到达,即
- 其余情况,上面两种方法都可以到达,即
dp[i][j] = dp[i][j - 1] + dp[i - 1][j]
- 边界情况
代码(C++)
int uniquePaths(int m, int n)
{
vector<vector<int>> dp(m, vector<int>(n));
for (int i = 0; i < m; ++i)
{
for (int j = 0; j < n; ++j)
{
if (i == 0 && j == 0) // 起点
dp[i][j] = 1;
else if (i == 0) // 上边界:只能从左边来
dp[i][j] = dp[i][j - 1];
else if (j == 0) // 左边界:只能从上面来
dp[i][j] = dp[i - 1][j];
else // 其余情况:从左边或上面来
dp[i][j] = dp[i][j - 1] + dp[i - 1][j];
}
}
return dp[m - 1][n - 1];
}
63. 不同路径 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
提示:
m == obstacleGrid.length
n == obstacleGrid[i].length
1 <= m, n <= 100
obstacleGrid[i][j]
为0
或1
题解
这题和 62. 不同路径 基本一样,只是加了一个障碍物而已,同样地采用动态规划解决,只需要在求 dp
数组时,遇到障碍物直接将那一格的到达路径数置为0即可。
dp
数组含义:dp[i][j]
表示达到位置 ( i , j ) (i, \enspace j) (i,j) 的路径数量- 初始化:起点为
(
0
,
0
)
(0, \enspace 0)
(0,0) ,显然只有唯一方法到达起点,即
dp[0][0] = 1
(除非有障碍物,也要置0) - 状态转移方程:
- 障碍物情况:有障碍物(
obstacleGrid[i][j] == 1
)的格子必然无法到达(注意这个要首先判断),即dp[i][j] = 0
- 边界情况
- 如果是上边界,要到达
(
i
,
j
)
(i, \enspace j)
(i,j) 显然只能通过左边的格子向右走一步到达,即
dp[i][j] = dp[i][j - 1]
- 如果是左边界,要到达
(
i
,
j
)
(i, \enspace j)
(i,j) 显然只能通过上面的格子向下走一步到达,即
dp[i][j] = dp[i - 1][j]
- 如果是上边界,要到达
(
i
,
j
)
(i, \enspace j)
(i,j) 显然只能通过左边的格子向右走一步到达,即
- 其余情况,上面两种方法都可以到达,即
dp[i][j] = dp[i][j - 1] + dp[i - 1][j]
- 障碍物情况:有障碍物(
代码(C++)
int uniquePathsWithObstacles(vector<vector<int>> &obstacleGrid)
{
vector<vector<int>> dp(obstacleGrid.size(), vector<int>(obstacleGrid[0].size()));
for (int i = 0; i < dp.size(); ++i)
{
for (int j = 0; j < dp[0].size(); ++j)
{
if (obstacleGrid[i][j]) // 有障碍物:无法到达
dp[i][j] = 0;
else if (i == 0 && j == 0) // 起点
dp[i][j] = 1;
else if (i == 0) // 上边界:只能从左边来
dp[i][j] = dp[i][j - 1];
else if (j == 0) // 左边界:只能从上面来
dp[i][j] = dp[i - 1][j];
else // 其余情况:从左边或上面来
dp[i][j] = dp[i][j - 1] + dp[i - 1][j];
}
}
return dp[dp.size() - 1][dp[0].size() - 1];
}
343. 整数拆分
题目描述
给定一个正整数 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。
提示:
2 <= n <= 58
题解
动态规划解决——虽然它的重叠子问题可能不那么明显。
对于任意一个数 n n n ,将其拆分时,我们可以先确定拆出来的某个正整数为 m m m ,则剩下的部分为 n − m n - m n−m ;而剩下的这部分 n − m n - m n−m ,我们可以直接将它与 m m m 相乘,也可以将它拆分后、用拆分后的乘积再与 m m m 相乘——显然,为了得到最终的最大乘积,我们应该哪个大取哪个。
注意,不一定拆分后的结果一定会更大。例如,如果此处 n − m = 3 n - m = 3 n−m=3 ,若拆分只能拆为 1 + 2 1 + 2 1+2 或 1 + 1 + 1 1 + 1 + 1 1+1+1 ,再求乘积为 2 2 2 或 1 1 1 ,都不如 3 3 3 本身大。
于是我们可以确定:
-
dp
数组的含义:dp[i]
表示将i
拆分能得到的最大乘积 -
初始化:参数最小值 2 2 2 只能拆成 1 + 1 1 + 1 1+1 ,求积为 1 1 1 ,即
dp[2] = 1
-
状态转移方程:
dp[i] = max(dp[i], max(j * (i - j), j * dp[i - j]))
,其中0 < j < i - 1
这里理论上应该是
0 < j < i
,但是当j = i - 1
时,相当于剩下的部分i - j
为1,而1是不可拆分的,相当于重复了一开始j = 1
时的情况,所以实际上可以将其省略。
代码(C++)
int integerBreak(int n)
{
vector<int> dp(n + 1);
dp[2] = 1;
for (int i = 3; i <= n; ++i) {
for (int j = 1; j < i - 1; ++j)
dp[i] = max(dp[i], max(j * (i - j), j * dp[i - j]));
}
return dp[n];
}
96. 不同的二叉搜索树
题目描述
给你一个整数 n
,求恰由 n
个节点组成且节点值从 1
到 n
互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。
示例 1:
输入:n = 3
输出:5
示例 2:
输入:n = 1
输出:1
提示:
1 <= n <= 19
题解
动态规划解决,要动点脑子的那种 💭
由于二叉搜索树的特性,我们很难一下找出什么种数与 n
之间的关系,或者 n
与 n - 1
分别对应的树形之间的关系。不妨尝试一下从基本情况开始考虑:
当 n = 0
时,只有空树这一种情况。
当 n = 1
时,只有平凡树这一种情况。
当 n = 2
时,有下面两种情况:
1 2
\ /
2 1
当 n = 3
时,有上面示例1种的5种情况,即:
图片来源:代码随想录
可以发现,我们可以把 n = 3
的情况进一步分为3种:
- 以1为根节点时,左子树为空、右子树的“布局”和
n = 2
时的两种情况相同 - 以2为跟节点时,左右子树都不为空,且“布局”和
n = 1
时的情况相同 - 以3为根节点时,右子树为空、左子树的“布局”和
n = 2
时的两种情况相同
这里强调“布局”,即(子)树的“形状”,是因为根据二叉搜索树的性质,对于同样长度的数列(无重复数字),无论具体数字有何差异,生成的二叉搜索树的可能形状都是相同的——它只关注数字间的大小关系,而不是具体的数值。
此时 2 = n - 1
、 1 = n - 2
,貌似我们找到了可以由重叠子问题推导的方法——即通过 dp[i - 1]
和 dp[i - 2]
推导出 dp[i]
的某种算法。
进一步,上述三种情况里,“空子树”其实也可以对应 n = 0
时的情况,于是它们仨可以总结为:
元素1为根节点搜索树的数量 = (右子树)2个元素的搜索树数量 * (左子树)0个元素的搜索树数量
元素2为根结点搜索树的数量 = (右子树)1个元素的搜索树数量 * (左子树)1个元素的搜索树数量
元素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] = sum(dp[j - 1] * dp[i - j])
,其中 0 < j < i
。
接下来,我们采用更严谨的方法证明上面的结论。
首先定义一下基本变量。之前说过,二叉搜索树的布局/形状只和生成之的数列长度有关,于是记 长度为 n n n 的数列生成的二叉搜索树的种数 为 S ( n ) S(n) S(n) 。
同时,记 长度为 n n n 的递增数列生成的、以数列中某个数 m m m 为根节点的二叉搜索树的种数 为 C ( n , m ) C(n, m) C(n,m) 。考虑这棵树的子树,显然左子树的种数可以表示为 S ( m − 1 ) S(m - 1) S(m−1) 、右子树的种树可以表示为 S ( n − m ) S(n - m) S(n−m) ,所以根据左右子树的排列组合,有
C ( n , m ) = S ( m − 1 ) ⋅ S ( n − m ) ( 1 ) C(n, m) = S(m - 1) \cdot S(n - m) \qquad (1) C(n,m)=S(m−1)⋅S(n−m)(1)
而对于由 1 , 2 , . . . , n 1, 2, ... , n 1,2,...,n 生成的二叉搜索树(题目所求),分别以其中各数为根节点,显然有
S ( n ) = ∑ m = 1 n C ( n , m ) ( 2 ) S(n) = \sum_{m = 1}^{n}C(n, m) \qquad (2) S(n)=m=1∑nC(n,m)(2)
将 ( 1 ) (1) (1) 带入 ( 2 ) (2) (2) ,得
S ( n ) = ∑ m = 1 n S ( m − 1 ) ⋅ S ( n − m ) S(n) = \sum_{m = 1}^{n} S(m - 1) \cdot S(n - m) S(n)=m=1∑nS(m−1)⋅S(n−m)
这与我们上面得到的状态转移方程一致,其中
S
(
n
)
S(n)
S(n) 其实就是我们熟悉的 dp[n]
了。
代码(C++)
int numTrees(int n)
{
vector<int> dp(n + 1);
dp[0] = 0, dp[1] = 1;
for (int i = 2; i <= n; ++i) {
for (int j = 1; j <= i; ++j)
dp[i] += dp[j - 1] * dp[i - j];
}
return dp[n];
}