对前端开发者而言,学习算法绝非为了"炫技"。它是你从"页面构建者"迈向"复杂系统设计者"的关键阶梯。它将你的编码能力从"实现功能"提升到"设计优雅、高效解决方案"的层面。从现在开始,每天投入一小段时间,结合前端场景去理解和练习,你将会感受到自身技术视野和问题解决能力的质的飞跃。------ 算法:资深前端开发者的进阶引擎
LeetCode 62. 不同路径
1. 题目描述
一个机器人位于一个 m × n 网格的左上角(起始点标记为"Start")。机器人每次只能向下或向右移动一步。机器人试图到达网格的右下角(标记为"Finish")。问总共有多少条不同的路径?
示例 1:
输入: m = 3, n = 2
输出: 3
解释:
从左上角到右下角一共有 3 条路径:
1. 右 → 右 → 下
2. 右 → 下 → 右
3. 下 → 右 → 右
示例 2:
输入: m = 7, n = 3
输出: 28
提示:
1 <= m, n <= 100- 题目数据保证答案小于等于
2 × 10^9
2. 问题分析
2.1 问题本质
这是一个典型的网格路径计数问题,属于动态规划的经典场景。我们可以将网格看作一个二维数组,机器人的移动方向被限制为只能向下或向右,这确保了路径不会形成环路。
2.2 前端视角思考
前端开发者经常处理类似的网格布局问题:
- 页面布局网格系统(如Bootstrap栅格)
- Canvas/Canvas2D中的坐标系统
- 游戏开发中的地图网格(如寻路算法)
- CSS Grid布局中的单元格遍历
理解此类问题的解决方法,有助于处理如:
- 计算DOM元素的不同排列方式
- 可视化图表中的路径规划
- 富文本编辑器的光标移动计算
3. 解题思路
3.1 思路概览
对于此类网格路径问题,常见的解题思路有:
- 动态规划(DP):最优解,时间复杂度 O(m×n),空间复杂度可优化
- 数学组合数法:利用组合数学公式直接计算
- 递归回溯:暴力搜索所有路径(性能差,仅适用于小规模问题)
3.2 思路详解
3.2.1 动态规划(最优解)
核心思想:到达每个格子的路径数 = 到达上方格子的路径数 + 到达左方格子的路径数
状态定义:
dp[i][j]表示从起点 (0,0) 到达格子 (i,j) 的不同路径数
状态转移方程:
dp[i][j] = dp[i-1][j] + dp[i][j-1]
边界条件:
- 第一行所有格子:只有一种路径(一直向右)
- 第一列所有格子:只有一种路径(一直向下)
3.2.2 组合数学法
核心思想 :从起点到终点需要移动 m+n-2 步,其中向下 m-1 步,向右 n-1 步。路径总数等于从 m+n-2 步中选择 m-1 步向下(或 n-1 步向右)的组合数。
公式:
C(m+n-2, m-1) = C(m+n-2, n-1)
3.2.3 递归回溯
核心思想:深度优先搜索所有可能路径,但会重复计算,效率低。
4. 代码实现
4.1 动态规划(基础版)
javascript
/**
* 动态规划基础版 - 二维数组存储
* 时间复杂度: O(m×n)
* 空间复杂度: O(m×n)
*/
function uniquePathsBasic(m, n) {
// 创建 m×n 的二维数组,初始化为 1(因为第一行和第一列都是1)
const dp = Array(m).fill().map(() => Array(n).fill(1));
// 从第2行第2列开始计算(因为第一行和第一列已经初始化为1)
for (let i = 1; i < m; i++) {
for (let j = 1; j < n; j++) {
// 状态转移方程:当前格子 = 上方格子 + 左方格子
dp[i][j] = dp[i-1][j] + dp[i][j-1];
}
}
// 返回右下角格子的值
return dp[m-1][n-1];
}
4.2 动态规划(空间优化版)
javascript
/**
* 动态规划空间优化版 - 滚动数组
* 时间复杂度: O(m×n)
* 空间复杂度: O(n) 或 O(m),取决于实现
*
* 原理:每次计算新的一行时,只需要上一行的数据
*/
function uniquePathsOptimized(m, n) {
// 如果m或n为1,只有1条路径
if (m === 1 || n === 1) return 1;
// 使用一维数组存储当前行的路径数
// 初始化为1,代表第一行的所有格子都只有1条路径
let dp = Array(n).fill(1);
// 从第2行开始计算
for (let i = 1; i < m; i++) {
// 从第2列开始计算
for (let j = 1; j < n; j++) {
// dp[j] 更新前代表上一行第j列的值(即上方格子)
// dp[j-1] 代表当前行第j-1列的值(即左方格子)
dp[j] = dp[j] + dp[j-1];
}
}
// 返回最后一列的值
return dp[n-1];
}
4.3 组合数学法
javascript
/**
* 组合数学法
* 时间复杂度: O(min(m, n))
* 空间复杂度: O(1)
*
* 使用公式: C(m+n-2, m-1) = C(m+n-2, n-1)
* 选择较小的维度计算以提高效率
*/
function uniquePathsMath(m, n) {
// 确保计算组合数时使用较小的数,减少计算量
const totalSteps = m + n - 2; // 总步数
const downSteps = m - 1; // 向下步数
const rightSteps = n - 1; // 向右步数
const k = Math.min(downSteps, rightSteps); // 选择较小的计算
// 计算组合数 C(totalSteps, k)
let result = 1;
// 使用组合数计算公式: C(n, k) = n! / (k! * (n-k)!)
// 可以简化为: C(n, k) = ∏(i=1 to k) (n - k + i) / i
for (let i = 1; i <= k; i++) {
result = result * (totalSteps - k + i) / i;
}
// 注意:由于JavaScript浮点数精度问题,结果可能不是整数
// 但在这个问题中,路径数一定是整数,所以可以取整
return Math.round(result);
}
// 示例验证
console.log(uniquePathsMath(3, 2)); // 输出: 3
console.log(uniquePathsMath(7, 3)); // 输出: 28
console.log(uniquePathsMath(10, 10)); // 输出: 48620
4.4 递归法(仅用于理解)
javascript
/**
* 递归法 - 仅用于理解问题,实际会超时
* 时间复杂度: O(2^(m+n)),指数级增长
* 空间复杂度: O(m+n),递归栈深度
*/
function uniquePathsRecursive(m, n) {
// 边界条件:到达第一行或第一列的任意位置,只有1条路径
if (m === 1 || n === 1) {
return 1;
}
// 递归计算:向右走 + 向下走
return uniquePathsRecursive(m - 1, n) + uniquePathsRecursive(m, n - 1);
}
// 示例:注意只能用于小规模计算
console.log(uniquePathsRecursive(3, 2)); // 输出: 3
// console.log(uniquePathsRecursive(7, 3)); // 计算较慢,不推荐
4.5 递归+记忆化(优化递归)
javascript
/**
* 递归+记忆化搜索
* 时间复杂度: O(m×n)
* 空间复杂度: O(m×n)
*
* 使用备忘录避免重复计算
*/
function uniquePathsMemo(m, n) {
// 创建备忘录,初始化为0表示未计算
const memo = Array(m + 1).fill().map(() => Array(n + 1).fill(0));
// 辅助递归函数
function dfs(i, j) {
// 边界条件
if (i === 1 || j === 1) {
return 1;
}
// 如果已经计算过,直接返回
if (memo[i][j] !== 0) {
return memo[i][j];
}
// 计算并存储结果
memo[i][j] = dfs(i - 1, j) + dfs(i, j - 1);
return memo[i][j];
}
return dfs(m, n);
}
// 示例
console.log(uniquePathsMemo(3, 2)); // 输出: 3
console.log(uniquePathsMemo(7, 3)); // 输出: 28
5. 各实现思路对比
| 方法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|---|
| 动态规划(基础) | O(m×n) | O(m×n) | 思路直观,易于理解 | 空间占用较大 | 通用场景,教学演示 |
| 动态规划(优化) | O(m×n) | O(min(m,n)) | 空间效率高 | 代码稍复杂 | 实际生产环境 |
| 组合数学法 | O(min(m,n)) | O(1) | 时间复杂度最低 | 可能溢出,需处理大数 | m,n较大时 |
| 递归法 | O(2^(m+n)) | O(m+n) | 代码简洁 | 指数级时间,无法实用 | 仅用于理解问题 |
| 递归+记忆化 | O(m×n) | O(m×n) | 递归思路,易于理解 | 递归栈可能溢出 | 中等规模问题 |
6. 总结与扩展
6.1 通用解题模板
对于网格路径类问题,可以遵循以下模板:
javascript
// 动态规划通用框架
function gridPathDP(m, n) {
// 1. 创建DP数组(考虑空间优化)
const dp = Array(m).fill().map(() => Array(n).fill(0));
// 2. 初始化边界条件
for (let i = 0; i < m; i++) dp[i][0] = 1;
for (let j = 0; j < n; j++) dp[0][j] = 1;
// 3. 状态转移
for (let i = 1; i < m; i++) {
for (let j = 1; j < n; j++) {
// 根据具体问题调整状态转移方程
dp[i][j] = dp[i-1][j] + dp[i][j-1];
}
}
// 4. 返回结果
return dp[m-1][n-1];
}
6.2 前端应用场景
- UI布局计算:计算不同屏幕尺寸下的布局排列方式
- 游戏开发:网格地图的寻路算法(如A*算法的简化版)
- 可视化:力导向图中节点间的不同连接方式
- 路由规划:计算从A点到B点的不同导航路径
6.3 类似题目推荐
-
LeetCode 63. 不同路径 II - 网格中有障碍物
- 需要在动态规划中处理障碍物情况
- 状态转移时,遇到障碍物则路径数为0
-
LeetCode 64. 最小路径和 - 寻找最小路径和
- 从路径计数变为路径和最小化
- 状态转移:
dp[i][j] = grid[i][j] + min(dp[i-1][j], dp[i][j-1])
-
LeetCode 120. 三角形最小路径和 - 三角形网格
- 二维DP的变体,需要注意边界条件
-
LeetCode 221. 最大正方形 - 寻找最大正方形面积
- 同样是网格DP,但状态定义和转移更复杂