【每日算法】LeetCode 62. 不同路径(多维动态规划)

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

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 思路概览

对于此类网格路径问题,常见的解题思路有:

  1. 动态规划(DP):最优解,时间复杂度 O(m×n),空间复杂度可优化
  2. 数学组合数法:利用组合数学公式直接计算
  3. 递归回溯:暴力搜索所有路径(性能差,仅适用于小规模问题)

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 前端应用场景

  1. UI布局计算:计算不同屏幕尺寸下的布局排列方式
  2. 游戏开发:网格地图的寻路算法(如A*算法的简化版)
  3. 可视化:力导向图中节点间的不同连接方式
  4. 路由规划:计算从A点到B点的不同导航路径

6.3 类似题目推荐

  1. LeetCode 63. 不同路径 II - 网格中有障碍物

    • 需要在动态规划中处理障碍物情况
    • 状态转移时,遇到障碍物则路径数为0
  2. LeetCode 64. 最小路径和 - 寻找最小路径和

    • 从路径计数变为路径和最小化
    • 状态转移:dp[i][j] = grid[i][j] + min(dp[i-1][j], dp[i][j-1])
  3. LeetCode 120. 三角形最小路径和 - 三角形网格

    • 二维DP的变体,需要注意边界条件
  4. LeetCode 221. 最大正方形 - 寻找最大正方形面积

    • 同样是网格DP,但状态定义和转移更复杂
相关推荐
不会就选b5 小时前
算法日常・每日刷题--<贪心>14
算法
AlienZHOU5 小时前
AI Coding 时代下,我的技术面试实践分享
前端·后端·面试
mmmmath_37 小时前
LeetCode.541.反转字符串II
数据结构·算法·leetcode
Navigator_Z7 小时前
LeetCode //MySQL - 1251. Average Selling Price
c语言·算法·leetcode
Captaincc8 小时前
AI用量v0.1.11更新发布 新增 jusage doctor 诊断指令 托盘展示token 和余额 新增 AutoClaw 支持
前端·后端·vibecoding
醇氧8 小时前
MySQL 8.0 系统表损坏与引擎转换故障排查实战
数据结构·算法
大熊背9 小时前
《Color constancy by characterization of illumination chromaticity》之色度色域最大化算法(二)
算法·白平衡·色度·色温
钓鱼的肝9 小时前
梳理(1-5)
c++·经验分享·笔记·算法·青少年编程
参.商.9 小时前
【Day 53】76. 最小覆盖子串
leetcode·golang