代码随想录算法训练营 Day29 | 动态规划 part02

62. 不同路径

一个机器人位于一个 m x n网格的左上角 (起始点在下图中标记为 "Start" )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 "Finish" )。

问总共有多少条不同的路径?

cpp 复制代码
class Solution {
public:
    int uniquePaths(int m, int n) {
        // 1. 定义 DP 数组
        // dp[i][j] 表示:从起点 (0,0) 走到 (i,j) 位置的不同路径数量
        vector<vector<int>> dp(m, vector<int>(n, 0));
        // 2. 初始化边界条件
        // 第一列:只能从上面往下走,只有一种路径
        for (int i = 0; i < m; i++) dp[i][0] = 1;
        // 第一行:只能从左边往右走,只有一种路径
        for (int j = 0; j < n; j++) dp[0][j] = 1;
        // 3. 状态转移(填表)
        // 遍历顺序:从上到下,从左到右(因为当前位置依赖上边和左边的数据)
        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                // 递推公式:
                // 走到 (i,j) 只能从上面 (i-1,j) 或左边 (i,j-1) 过来
                // 所以路径数 = 上面过来的路径数 + 左边过来的路径数
                dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
            }
        }
        // 4. 返回结果:终点位置的路径数
        return dp[m - 1][n - 1];
    }
};

总结

1. 二维 DP 的解题套路

与一维 DP 类似,但需要处理两个维度的遍历:

  • 定义:dp[i][j] 代表终点的状态。
  • 递推公式:dp[i][j] = dp[i-1][j] + dp[i][j-1]。这是组合数学中的加法原理。
  • 遍历顺序:因为是 i 依赖 i-1j 依赖 j-1,所以必须 先算出上边和左边的值。双重循环的顺序(从上到下、从左到右)是正确的。
2. 复杂度分析
  • 时间复杂度:O(m×n)。需要遍历整个二维矩阵。
  • 空间复杂度:O(m×n)。需要一个二维矩阵存储所有状态。

63. 不同路径 II

给定一个 m x n 的整数数组 grid。一个机器人初始位于 左上角(即 grid[0][0])。机器人尝试移动到 右下角(即 grid[m - 1][n - 1])。机器人每次只能向下或者向右移动一步。

网格中的障碍物和空位置分别用 10 来表示。机器人的移动路径中不能包含 任何 有障碍物的方格。

返回机器人能够到达右下角的不同路径数量。

测试用例保证答案小于等于 2 * 109

cpp 复制代码
class Solution {
public:
    int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
        int m = obstacleGrid.size();
        int n = obstacleGrid[0].size();
        // 1. 特殊情况判断
        // 如果起点或终点本身就有障碍物,直接无法到达
        if (obstacleGrid[0][0] == 1 || obstacleGrid[m - 1][n - 1] == 1) return 0;
        // 2. 定义 DP 数组
        vector<vector<int>> dp(m, vector<int>(n, 0));
        // 3. 初始化第一列
        // 遇到障碍物后,后面的格子都无法通过,直接 break
        for (int i = 0; i < m; i++) {
            if (obstacleGrid[i][0] == 1) break;
            dp[i][0] = 1;
        }
        // 4. 初始化第一行
        // 同理,遇到障碍物后,后面的格子都无法通过
        for (int j = 0; j < n; j++) {
            if (obstacleGrid[0][j] == 1) break;
            dp[0][j] = 1;
        }
        // 5. 状态转移
        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                // 只有当前格子不是障碍物时,才进行递推
                if (obstacleGrid[i][j] == 0) {
                    dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
                }
                // 如果是障碍物,dp[i][j] 保持为 0(初始值),表示路径数为 0
            }
        }
        return dp[m - 1][n - 1];
    }
};

总结

1. 易错点分析
  • 错误写法:不管有没有障碍物,把第一行第一列全初始化为 1,遇到障碍物时才设为 0。
    • 后果:障碍物后面的格子本应是 0(不可达),却被错误地初始化成了 1。
  • 正确写法:使用 break。一旦遇到障碍物,循环终止,障碍物及其后面的格子都保持初始值 0。这是最标准、最高效的初始化方式。
2. 复杂度分析
  • 时间复杂度:O(m×n)。遍历整个网格。
  • 空间复杂度:O(m×n)。存储 DP 表。

343. 整数拆分

给定一个正整数 n ,将其拆分为 k 个 正整数 的和( k >= 2 ),并使这些整数的乘积最大化。

返回 你可以获得的最大乘积

cpp 复制代码
class Solution {
public:
    int integerBreak(int n) {
        // 1. 定义 DP 数组
        // dp[i] 表示:将整数 i 拆分成至少两个正整数后,获得的最大乘积
        vector<int> dp(n + 1, 0);
        // 2. 初始化
        // dp[0] 和 dp[1] 无意义,因为没法拆分
        dp[2] = 1; // 2 拆分成 1+1,乘积为 1
        // 3. 状态转移
        // 从 3 开始遍历到 n
        for (int i = 3; i <= n; i++) {
            // 遍历拆分的第一个整数 j
            // j 的范围是 [1, i/2] (对称性,拆分超过一半就重复了)
            for (int j = 1; j <= i / 2; j++) {
                // 核心递推公式:
                // 我们有两种选择来计算乘积:
                // 1. 只拆成两个数:j * (i-j)
                // 2. 拆成多个数:j * dp[i-j] (即对 i-j 继续拆分,取其最大乘积)
                // 需要在两者中取最大值,同时也要和之前的 dp[i] 比较取最大
                dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j));
            }
        }
        return dp[n];
    }
};

总结

1. 递推公式深度解析

当我们把整数 i 拆分成 ji-j 两部分时:

  • 情况一:不再拆分。
    • 直接计算乘积:j * (i - j)
  • 情况二:继续拆分。
    • j 保持不变,将 i - j 继续拆分成更多份。
    • 此时乘积为:j * dp[i - j](利用之前计算过的最优解)。
2. 遍历范围的优化

使用 j <= i/2,这是一个很好的优化点。

因为拆分具有对称性(比如拆 10,拆成 3+7 和 7+3 是一样的),所以只需遍历到一半即可。如果不写 i/2 写成 j < i 也是对的,只是会多做无用功。

3. 复杂度分析
  • 时间复杂度:O(n2)。两层循环。
  • 空间复杂度:O(n)。

96. 不同的二叉搜索树

给你一个整数 n ,求恰由 n 个节点组成且节点值从 1n 互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。

cpp 复制代码
class Solution {
public:
    int numTrees(int n) {
        // 1. 定义 DP 数组
        // dp[i] 表示:由 i 个节点组成的互不相同的 BST 的数量
        vector<int> dp(n + 1, 0);
        // 2. 初始化
        // dp[0] = 1 代表空树也是一种 BST
        // 这是一个数学上的辅助项,保证递推公式能跑通(乘法中的 1)
        dp[0] = 1;
        // 3. 状态转移
        // 遍历每一个节点总数 i
        for (int i = 1; i <= n; i++) {
            // 遍历每一个可能的根节点 j
            for (int j = 1; j <= i; j++) {
                // 递推公式:
                // 以 j 为根节点:
                // 左子树有 j-1 个节点,方案数为 dp[j-1]
                // 右子树有 i-j 个节点,方案数为 dp[i-j]
                // 乘积就是以 j 为根时的 BST 总数
                dp[i] += dp[j - 1] * dp[i - j];
            }
        }
        return dp[n];
    }
};

总结

1. 核心思路:以谁为根?

要把 i 个节点组成 BST,关键在于 "根节点是谁"。

  • 如果根节点是 j
    • j 小的 j-1 个节点只能放在左子树。
    • j 大的 i-j 个节点只能放在右子树。
  • 组合问题:左子树有 dp[j-1] 种摆法,右子树有 dp[i-j] 种摆法,根据乘法原理,总数就是两者相乘。
  • 累加:因为 j 可以取 1i,所以要把所有情况加起来。
2. 复杂度分析
  • 时间复杂度:O(n2)。双层循环。
  • 空间复杂度:O(n)。
3. 为什么 dp[0] = 1

如果 dp[0] = 0,那么当 j=1 时,左子树节点数为 0,dp[0] * dp[i-1] 就会变成 0,导致结果错误。空树也是一种形态,所以必须初始化为 1。

相关推荐
样例过了就是过了2 小时前
LeetCode热题100 跳跃游戏 II
c++·算法·leetcode·贪心算法·动态规划
rit84324992 小时前
基于NSGA-II的多目标优化算法(MATLAB实现)
开发语言·算法·matlab
香蕉鼠片2 小时前
第三大的数
数据结构·算法·leetcode
汀、人工智能2 小时前
[特殊字符] 第28课:相交链表
数据结构·算法·链表·数据库架构··相交链表
计算机安禾2 小时前
【数据结构与算法】第32篇:交换排序(一):冒泡排序
c语言·数据结构·c++·算法·链表·排序算法·visual studio code
lxh01132 小时前
蜗牛排序题解
javascript·算法
airuike1232 小时前
高性能MEMS IMU:重构无人机飞行控制核心
人工智能·算法·重构·无人机
娇娇爱吃蕉蕉.2 小时前
类和对象的默认成员函数
c语言·开发语言·c++·算法
人道领域2 小时前
【LeetCode刷题日记】哈希表:从0基础到实战全解析
算法·leetcode·哈希算法