【每日算法】LeetCode 64. 最小路径和(多维动态规划)

对前端开发者而言,学习算法绝非为了"炫技"。它是你从"页面构建者"迈向"复杂系统设计者"的关键阶梯。它将你的编码能力从"实现功能"提升到"设计优雅、高效解决方案"的层面。从现在开始,每天投入一小段时间,结合前端场景去理解和练习,你将会感受到自身技术视野和问题解决能力的质的飞跃。------ 算法:资深前端开发者的进阶引擎

LeetCode 64. 最小路径和

1. 题目描述

给定一个包含非负整数的 m x n 网格 grid,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。

说明:每次只能向下或者向右移动一步。

示例 1

复制代码
输入:grid = [[1,3,1],[1,5,1],[4,2,1]]
输出:7
解释:因为路径 1→3→1→1→1 的总和最小,为 7。

示例 2

复制代码
输入:grid = [[1,2,3],[4,5,6]]
输出:12
解释:路径 1→2→3→6 的总和为 12。

提示

  • m == grid.length
  • n == grid[i].length
  • 1 <= m, n <= 200
  • 0 <= grid[i][j] <= 100

2. 问题分析

这个问题是典型的二维网格路径问题,具有以下特点:

  1. 只能向右或向下移动:这意味着到达每个点的路径只能来自上方或左方
  2. 求最小路径和:需要从所有可能路径中找到和最小的那条
  3. 非负整数网格:这个条件保证了贪心算法可能可行,但实际需要动态规划

前端视角:可以将其类比为在一个二维表格(如Excel)中,从左上角单元格移动到右下角单元格,每个单元格有成本,需要找到最低成本的路径。

3. 解题思路

3.1 思路一:暴力递归(回溯法)

通过递归尝试所有可能的路径,计算每条路径的和,取最小值。时间复杂度为指数级,不推荐。

3.2 思路二:记忆化搜索

在递归基础上添加记忆化,避免重复计算子问题。时间复杂度O(m×n),空间复杂度O(m×n)。

3.3 思路三:动态规划(最优解)

使用DP表存储到达每个位置的最小路径和,自底向上计算。时间复杂度O(m×n),空间复杂度可优化到O(n)或O(1)。

3.4 思路四:在原数组上修改(原地DP)

直接修改原数组,将空间复杂度优化到O(1)。这是最优解之一,适合允许修改原数组的场景。

最优解:动态规划,时间复杂度O(m×n),空间复杂度可优化到O(min(m,n))或O(1)。

4. 代码实现

4.1 基础动态规划(二维DP表)

javascript 复制代码
/**
 * 最小路径和 - 基础动态规划(二维DP表)
 * 时间复杂度:O(m×n)
 * 空间复杂度:O(m×n)
 * 
 * @param {number[][]} grid
 * @return {number}
 */
var minPathSum = function(grid) {
    const m = grid.length;      // 行数
    const n = grid[0].length;   // 列数
    
    // 创建DP表,dp[i][j]表示从(0,0)到(i,j)的最小路径和
    const dp = Array.from({ length: m }, () => new Array(n).fill(0));
    
    // 初始化起点
    dp[0][0] = grid[0][0];
    
    // 初始化第一列:只能从上方下来
    for (let i = 1; i < m; i++) {
        dp[i][0] = dp[i-1][0] + grid[i][0];
    }
    
    // 初始化第一行:只能从左方过来
    for (let j = 1; j < n; j++) {
        dp[0][j] = dp[0][j-1] + grid[0][j];
    }
    
    // 填充DP表:每个位置的最小路径和 = 当前值 + min(上方值, 左方值)
    for (let i = 1; i < m; i++) {
        for (let j = 1; j < n; j++) {
            dp[i][j] = grid[i][j] + Math.min(dp[i-1][j], dp[i][j-1]);
        }
    }
    
    return dp[m-1][n-1];
};

// 示例演示
const grid = [
    [1, 3, 1],
    [1, 5, 1],
    [4, 2, 1]
];

console.log(minPathSum(grid)); // 输出: 7

// 步骤分解(DP表填充过程):
// 初始:        | 计算后:
// [1, 3, 1]    | [1, 4, 5]
// [1, 5, 1]    | [2, 7, 6]
// [4, 2, 1]    | [6, 8, 7] ← 右下角7就是答案

4.2 空间优化动态规划(一维DP)

javascript 复制代码
/**
 * 最小路径和 - 空间优化动态规划(一维DP)
 * 时间复杂度:O(m×n)
 * 空间复杂度:O(n) - 只存储一行数据
 * 
 * 前端应用场景:处理大型表格数据时减少内存占用
 * 
 * @param {number[][]} grid
 * @return {number}
 */
var minPathSum = function(grid) {
    const m = grid.length;
    const n = grid[0].length;
    
    // 使用一维数组存储当前行的DP值
    const dp = new Array(n).fill(0);
    
    // 初始化第一行
    dp[0] = grid[0][0];
    for (let j = 1; j < n; j++) {
        dp[j] = dp[j-1] + grid[0][j];
    }
    
    // 逐行计算
    for (let i = 1; i < m; i++) {
        // 更新当前行的第一个元素
        dp[0] = dp[0] + grid[i][0];
        
        for (let j = 1; j < n; j++) {
            // dp[j] 当前是上一行的值(相当于dp[i-1][j])
            // dp[j-1] 是当前行已计算的值(相当于dp[i][j-1])
            dp[j] = grid[i][j] + Math.min(dp[j], dp[j-1]);
        }
    }
    
    return dp[n-1];
};

// 空间优化原理:
// 我们只关心当前行和上一行的数据,所以可以用一行来滚动更新
// dp[j]在更新前代表上一行的结果,更新后代表当前行的结果

4.3 原地修改(O(1)空间)

javascript 复制代码
/**
 * 最小路径和 - 原地修改(O(1)空间)
 * 时间复杂度:O(m×n)
 * 空间复杂度:O(1) - 直接在原数组上修改
 * 
 * 适用场景:可以修改输入数组,且需要极致空间优化
 * 
 * @param {number[][]} grid
 * @return {number}
 */
var minPathSum = function(grid) {
    const m = grid.length;
    const n = grid[0].length;
    
    // 直接修改原数组为DP表
    // 初始化第一列
    for (let i = 1; i < m; i++) {
        grid[i][0] += grid[i-1][0];
    }
    
    // 初始化第一行
    for (let j = 1; j < n; j++) {
        grid[0][j] += grid[0][j-1];
    }
    
    // 动态规划填充
    for (let i = 1; i < m; i++) {
        for (let j = 1; j < n; j++) {
            grid[i][j] += Math.min(grid[i-1][j], grid[i][j-1]);
        }
    }
    
    return grid[m-1][n-1];
};

4.4 记忆化搜索(递归+缓存)

javascript 复制代码
/**
 * 最小路径和 - 记忆化搜索
 * 时间复杂度:O(m×n) - 每个子问题计算一次
 * 空间复杂度:O(m×n) - 递归栈和记忆表
 * 
 * 适合理解动态规划思想的递归本质
 * 
 * @param {number[][]} grid
 * @return {number}
 */
var minPathSum = function(grid) {
    const m = grid.length;
    const n = grid[0].length;
    
    // 创建记忆表,初始化为-1表示未计算
    const memo = Array.from({ length: m }, () => new Array(n).fill(-1));
    
    /**
     * 递归函数:计算从(i,j)到右下角的最小路径和
     * @param {number} i - 当前行
     * @param {number} j - 当前列
     * @return {number}
     */
    const dfs = (i, j) => {
        // 越界检查
        if (i >= m || j >= n) return Infinity;
        
        // 到达终点
        if (i === m - 1 && j === n - 1) return grid[i][j];
        
        // 如果已经计算过,直接返回
        if (memo[i][j] !== -1) return memo[i][j];
        
        // 计算向下和向右的路径和
        const down = dfs(i + 1, j);
        const right = dfs(i, j + 1);
        
        // 选择较小的路径,并加上当前值
        memo[i][j] = grid[i][j] + Math.min(down, right);
        
        return memo[i][j];
    };
    
    return dfs(0, 0);
};

5. 复杂度与优缺点对比

方法 时间复杂度 空间复杂度 优点 缺点 适用场景
基础DP(二维) O(m×n) O(m×n) 逻辑清晰,易于理解 空间占用较大 教学、小规模数据
空间优化DP(一维) O(m×n) O(n) 空间效率高,逻辑清晰 代码稍复杂 大规模数据,内存敏感
原地修改DP O(m×n) O(1) 极致空间优化 修改原数据 可修改输入,内存极限优化
记忆化搜索 O(m×n) O(m×n) 递归思路直观 递归栈可能溢出 理解DP递归本质

6. 总结与拓展

6.1 动态规划模板

javascript 复制代码
/**
 * 二维网格路径问题通用模板
 * 适用条件:只能向右/向下移动,求最优路径
 */
function gridDP(grid) {
    const m = grid.length, n = grid[0].length;
    
    // 1. 创建DP表
    const dp = new Array(m).fill(0).map(() => new Array(n).fill(0));
    
    // 2. 初始化边界
    dp[0][0] = grid[0][0];
    for (let i = 1; i < m; i++) dp[i][0] = dp[i-1][0] + grid[i][0];
    for (let j = 1; j < n; j++) dp[0][j] = dp[0][j-1] + grid[0][j];
    
    // 3. 状态转移
    for (let i = 1; i < m; i++) {
        for (let j = 1; j < n; j++) {
            dp[i][j] = grid[i][j] + Math.min(dp[i-1][j], dp[i][j-1]);
        }
    }
    
    // 4. 返回结果
    return dp[m-1][n-1];
}

6.2 前端应用场景

  1. 资源加载优化:类似于CDN节点选择最优路径
  2. UI渲染优化:组件树渲染成本最小化
  3. 游戏开发:地图寻路算法(网格游戏)
  4. 数据可视化:热力图路径分析

6.3 类似题目推荐

  1. LeetCode 62. 不同路径 - 计算路径数量
  2. LeetCode 63. 不同路径 II - 带有障碍物的版本
  3. LeetCode 120. 三角形最小路径和 - 三角形结构的最小路径
  4. LeetCode 174. 地下城游戏 - 逆向动态规划
  5. LeetCode 221. 最大正方形 - 二维DP求最大面积
  6. LeetCode 931. 下降路径最小和 - 类似问题,方向不同
相关推荐
自学不成才10 分钟前
深度复盘:一次flutter应用基于内存取证的黑盒加密破解实录并完善算法推理助手
c++·python·算法·数据挖掘
June`1 小时前
全排列与子集算法精解
算法·leetcode·深度优先
徐先生 @_@|||1 小时前
Palantir Foundry 五层架构模型详解
开发语言·python·深度学习·算法·机器学习·架构
夏鹏今天学习了吗2 小时前
【LeetCode热题100(78/100)】爬楼梯
算法·leetcode·职场和发展
圣保罗的大教堂2 小时前
leetcode 712. 两个字符串的最小ASCII删除和 中等
leetcode
m0_748250033 小时前
C++ 信号处理
c++·算法·信号处理
Ro Jace3 小时前
电子侦察信号处理流程及常用算法
算法·信号处理
yuyanjingtao3 小时前
动态规划 背包 之 凑钱
c++·算法·青少年编程·动态规划·gesp·csp-j/s
core5124 小时前
SGD 算法详解:蒙眼下山的寻宝者
人工智能·算法·矩阵分解·sgd·目标函数