24暑假算法刷题 | Day34 | 动态规划 II | LeetCode 62. 不同路径,63. 不同路径 II,343. 整数拆分,96. 不同的二叉搜索树


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]
    • 其余情况,上面两种方法都可以到达,即 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”)。

现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?

网格中的障碍物和空位置分别用 10 来表示。

示例 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]01

题解

这题和 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]
    • 其余情况,上面两种方法都可以到达,即 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 nm ;而剩下的这部分 n − m n - m nm ,我们可以直接将它与 m m m 相乘,也可以将它拆分后、用拆分后的乘积再与 m m m 相乘——显然,为了得到最终的最大乘积,我们应该哪个大取哪个。

注意,不一定拆分后的结果一定会更大。例如,如果此处 n − m = 3 n - m = 3 nm=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 个节点组成且节点值从 1n 互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。

示例 1:

在这里插入图片描述

输入:n = 3
输出:5

示例 2:

输入:n = 1
输出:1

提示:

  • 1 <= n <= 19

题解

动态规划解决,要动点脑子的那种 💭 ​

由于二叉搜索树的特性,我们很难一下找出什么种数与 n 之间的关系,或者 nn - 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 - 11 = 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(m1) 、右子树的种树可以表示为 S ( n − m ) S(n - m) S(nm) ,所以根据左右子树的排列组合,有

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(m1)S(nm)(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=1nC(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=1nS(m1)S(nm)

这与我们上面得到的状态转移方程一致,其中 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];
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值