代码随想录算法训练营 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。

相关推荐
江畔柳前堤3 小时前
大语言模型分布式训练:从并行策略到万卡工程的系统梳理
人工智能·分布式·深度学习·算法·目标检测·机器学习·语言模型
Forever Nore4 小时前
LeetCode 4 寻找两个正序数组的中位数 - 二分
算法·leetcode
罗西的思考6 小时前
【OpenClaw具身硬件】MiniClaw 阅读笔记---(1)基础
人工智能·算法·机器学习
蛋先生DX6 小时前
大模型参数存储格式揭秘:BF不是男朋友
深度学习·算法·llm
爱跳舞的烤冷面7 小时前
自学嵌入式第22天(数据结构——哈希)
数据结构·算法·哈希算法
猎嘤一号7 小时前
博弈论(Game Theory)的理论、算法与工程
人工智能·算法·安全·博弈论
月光船幽幽7 小时前
影子模式下保护 logits 不被修改
人工智能·python·算法
马拉AI7 小时前
腾讯开源 Agent 记忆系统,AI“换对话就忘”的问题有了新解法(附安装使用教程)
人工智能·算法·开源·科研
地平线开发者9 小时前
征程6工具链模型X86推理方式说明
算法
地平线开发者9 小时前
【征程6】校准量化中HistogramObserver解析
算法