目录
一、做题心得
今天是动态规划章节打卡的第二天,探讨了了经典的路径问题以及整数拆分还有二叉搜索树种类数的问题。今天做题最大的感受就是要学会举例判断,画图,还有计算--其实感觉就是数学思想在编程里的运用。
直接开始今天的内容。
二、题目与题解
题目一: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
题解:动态规划
这道题很经典,算是棋盘,路径这一类问题的重要基础。
题目中明确机器人只能向下或者向右移动(很容易想到中国象棋里的卒),那么我们就可以确定,要到达(i, j)这个位置,可以从左边(i - 1, j)或者上边(i, j - 1)走一步得来,即走到当前位置的路径数就是这两个位置的路径数之和。
这里需要注意的就是初始化:第一行或者第一列都只能通过只向下或者只向右得到,故都只会有一条路径实现。
代码如下:
class Solution {
public:
int uniquePaths(int m, int n) {
vector<vector<int>> dp(m, vector<int>(n, 0)); //dp数组:dp[i][j]表示从起点(0, 0)处到(i, j)处的路径数
//初始化dp:最左边一列和最上边一行路径数都为1(只能向下或只能向右)
for (int i = 0; i < m; i++) { //第一列
dp[i][0] = 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];
}
};
题目二: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
题解:动态规划
这个题相对来说就复杂得多,加上了障碍物。遇到这种有障碍物的问题,我们都把障碍物处的路径数记为0(因为你不能从障碍物处走到下个位置)-- 这里我们初始化dp数组的时候一般都会考虑,然后每次循环计算的时候也不考虑计算有障碍物的位置。
注意:第一行与第一列的初始化:一旦出现障碍物,后边的所有格子全都过不去,即后边的格子路径数都为0 -- 这里由于dp数组已经初始化为0,这里直接 break 即可。
class Solution {
public:
int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
int m = obstacleGrid.size(); //行数
int n = obstacleGrid[0].size(); //列数
vector<vector<int>> dp(m, vector<int>(n, 0)); //dp[i][j]:表示从起始点(0, 0)到(i, j)的路径数
if (obstacleGrid[0][0] == 1 || obstacleGrid[m - 1][n - 1] == 1) { //起点或者终点有障碍物
return 0;
}
for (int i = 0; i < m; i++) { //初始化第一列
if (obstacleGrid[i][0] == 0) {
dp[i][0] = 1;
}
else break; //注意:一旦出现障碍物,第一列障碍物及下边的格子就走不过去了,默认路径为0--终止循环
}
for (int j = 0; j < n; j++) { //初始化第一行
if (obstacleGrid[0][j] == 0) {
dp[0][j] = 1;
}
else break; //同理:第一行出现障碍物后,后边的格子就过不去
}
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
if (obstacleGrid[i][j] == 0) { //当(i, j)没有障碍的时候,再推导dp[i][j]
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
}
return dp[m - 1][n - 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
题解:动态规划
这道题,个人感觉比较难,不容易想到。
我们要考虑不同的拆分情况,还要筛选出,这些情况的最大值。
对于一个数的拆分,我们可以将当前数字 i 拆分成 j 和 i - j,其中 j 范围:[1, i - 1]。通过循环将每次拆分得到的结果取最大值。--这里需要注意的是:当前按照 j 拆分之后,i - j 还有可能可以继续拆分,所有我们要将两种情况的结果进行比较。
即:
两种拆分方式:
1. 拆分后不可再分:将 i 拆分成 j 和 i−j 的和,且 i−j 不再拆分成多个正整数,此时的乘积是 j×(i−j)
2. 拆分后还可以分:将 i 拆分成 j 和 i−j 的和,且 i−j 继续拆分成多个正整数,此时的乘积是 j×dp[i−j]
代码如下:
class Solution {
public:
int integerBreak(int n) {
vector<int> dp(n + 1); //dp[i]表示整数i拆分后的最大乘积
dp[2] = 1; //初始化dp[2],2只能拆分成 1 + 1
for (int i = 3; i <= n ; i++) {
for (int j = 1; j < i; j++) { //遍历所有可能的拆分方式j:将 i 拆分成 j 和 i - j
dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j)); //计算当前拆分的两种可能乘积,并取较大值更新dp[i]
}
}
return dp[n];
}
};
题目四:96.不同的二叉搜索树
题目链接
给你一个整数
n
,求恰由n
个节点组成且节点值从1
到n
互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。示例 1:
输入:n = 3 输出:5示例 2:
输入:n = 1 输出:1提示:
1 <= n <= 19
题解:动态规划
这是动态规划在二叉树上的应用--准确来说是二叉搜索树。
首先回顾一下二叉搜索树的性质--节点值:左 < 根 <右
这样我们求有多少种二叉搜索树,就可以通过以不同值作为根节点,左子树的节点个数就是小于根节点值的节点个数,同理,右子树的节点个数就是大于根节点的值的个数。因此,我们就可以得出左右子树的节点个数。
由此可以得出动规的递推公式:dp[n] = dp[n - 1] + dp[n - 2] * dp[1] + dp[n - 3] * dp[2]...
我们可以通过循环得出以上递推公式的求和--注意这里初始化dp[0] = 1 用来防止计算造成 dp[n - 1] * dp[0]= 0(如果初始化为0的话)。
代码如下:
class Solution {
public:
int numTrees(int n) {
vector<int> dp(n + 1); //dp[i]表示:从 1 到 i 为节点组成的二叉搜索树的个数为 dp[i]
dp[0] = 1; //为了方便计算,我们将dp[0]设为1
dp[1] = 1;
for (int i = 2; i <= n; i++) {
for (int j = 1;j <= i; j++) { //选择j作为根节点
dp[i] += dp[j - 1] * dp[i - j]; // j-1 为 j 为头结点时左子树(1, j - 1)节点数量,i-j 为以 j 为头结点时右子树(j + 1, i)节点数量
}
}
return dp[n];
}
};
三、小结
今天的打卡到此也就结束了,学习了动态规划的一些经典的应用。