对前端开发者而言,学习算法绝非为了"炫技"。它是你从"页面构建者"迈向"复杂系统设计者"的关键阶梯。它将你的编码能力从"实现功能"提升到"设计优雅、高效解决方案"的层面。从现在开始,每天投入一小段时间,结合前端场景去理解和练习,你将会感受到自身技术视野和问题解决能力的质的飞跃。------ 算法:资深前端开发者的进阶引擎
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.lengthn == grid[i].length1 <= m, n <= 2000 <= grid[i][j] <= 100
2. 问题分析
这个问题是典型的二维网格路径问题,具有以下特点:
- 只能向右或向下移动:这意味着到达每个点的路径只能来自上方或左方
- 求最小路径和:需要从所有可能路径中找到和最小的那条
- 非负整数网格:这个条件保证了贪心算法可能可行,但实际需要动态规划
前端视角:可以将其类比为在一个二维表格(如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 前端应用场景
- 资源加载优化:类似于CDN节点选择最优路径
- UI渲染优化:组件树渲染成本最小化
- 游戏开发:地图寻路算法(网格游戏)
- 数据可视化:热力图路径分析
6.3 类似题目推荐
- LeetCode 62. 不同路径 - 计算路径数量
- LeetCode 63. 不同路径 II - 带有障碍物的版本
- LeetCode 120. 三角形最小路径和 - 三角形结构的最小路径
- LeetCode 174. 地下城游戏 - 逆向动态规划
- LeetCode 221. 最大正方形 - 二维DP求最大面积
- LeetCode 931. 下降路径最小和 - 类似问题,方向不同