LeetCode算法日记 - Day 71: 不同路径、不同路径II
目录
1. 不同路径
1.1 题目解析
1.2 解法
1.3 代码实现
2. 不同路径
2.1 题目解析
2.2 解法
2.3 代码实现
1. 不同路径
https://leetcode.cn/problems/unique-paths/
一个机器人位于一个 m x n
网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
示例 1:
输入:m = 3, n = 7 输出:28
示例 2:
输入:m = 3, n = 2 输出:3 解释: 从左上角开始,总共有 3 条路径可以到达右下角。 1. 向右 -> 向下 -> 向下 2. 向下 -> 向下 -> 向右 3. 向下 -> 向右 -> 向下
示例 3:
输入:m = 7, n = 3 输出:28
示例 4:
输入:m = 3, n = 3 输出:6
提示:
1 <= m, n <= 100
- 题目数据保证答案小于等于
2 * 109
1.1 题目解析
题目本质
这是一个典型的"路径计数"问题。机器人从起点 (1,1) 到终点 (m,n),每次只能向右或向下移动一步,求有多少种不同的走法。本质上是求"在约束条件下的方案数统计"。
常规解法
最直观的想法是用递归/DFS遍历所有可能路径:从起点开始,每个位置尝试向右和向下两个方向,递归到终点就算一条路径。
问题分析
纯递归会超时。假设 m=n=100,时间复杂度接近 O(2^(m+n)),存在大量重复计算。比如到达 (2,3) 这个位置的路径数会被反复计算。
思路转折
要想高效 → 必须避免重复计算 → 使用动态规划。
关键观察:到达某个格子 (i,j) 的路径数 = 到达其左边格子的路径数 + 到达其上边格子的路径数。因为机器人只能从左边或上边来。通过自底向上填表,每个状态只计算一次。
1.2 解法
算法思想
使用二维 DP,定义 dp[i][j] 表示从起点到达位置 (i,j) 的路径数。
递推公式:
dp[i][j] = dp[i-1][j] + dp[i][j-1]
边界条件:
-
dp[1][1] = 1(起点)
-
第一行和第一列都只有一条路径
i)创建 (m+1) × (n+1) 的 dp 数组,使用 1-索引避免边界判断
ii)初始化起点 dp[1][1] = 1
iii)双层循环遍历所有格子,按递推公式累加
iv)返回 dp[m][n] 作为答案
易错点
-
索引映射问题:使用 1-索引的 dp 数组时,起点是 dp[1][1] 而非 dp[0][0],这样可以自动处理边界(dp数组第0行第0列默认为0)
-
重复初始化:在循环中不要重复设置 dp[1][1] = 1,在循环前设置好,防止重复初始化。
1.3 代码实现
static class Solution {int[][] dp;public int uniquePaths(int m, int n) {dp = new int[m + 1][n + 1];dp[1][1] = 1; // 起点初始化for (int i = 1; i <= m; i++) {for (int j = 1; j <= n; j++) {if (i == 1 && j == 1) {continue; // 跳过起点}// 当前位置 = 左边来的 + 上边来的dp[i][j] = dp[i][j - 1] + dp[i - 1][j];}}return dp[m][n];}
}
复杂度分析
-
时间复杂度:O(m × n),需要填充整个 dp 表格
-
空间复杂度:O(m × n),使用了二维 dp 数组
2. 不同路径
https://leetcode.cn/problems/unique-paths-ii/description/
给定一个 m x n
的整数数组 grid
。一个机器人初始位于 左上角(即 grid[0][0]
)。机器人尝试移动到 右下角(即 grid[m - 1][n - 1]
)。机器人每次只能向下或者向右移动一步。
网格中的障碍物和空位置分别用 1
和 0
来表示。机器人的移动路径中不能包含 任何 有障碍物的方格。
返回机器人能够到达右下角的不同路径数量。
测试用例保证答案小于等于 2 * 109
。
示例 1:
输入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]]
输出:2
解释:3x3 网格的正中间有一个障碍物。
从左上角到右下角一共有 2
条不同的路径:
1. 向右 -> 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右 -> 向右
示例 2:
输入:obstacleGrid = [[0,1],[0,0]] 输出:1
提示:
m == obstacleGrid.length
n == obstacleGrid[i].length
1 <= m, n <= 100
obstacleGrid[i][j]
为0
或1
2.1 题目解析
题目本质
这是"不同路径"问题的升级版,增加了障碍物约束。本质上是"带约束条件的路径计数"问题:在网格中有障碍物的情况下,统计从左上角到右下角的所有可行路径数。
常规解法
最直观的想法是递归/DFS:从起点开始,遇到障碍物就回退,否则尝试向右和向下两个方向,递归到终点计数。
问题分析
纯递归会超时。对于 100×100 的网格,最坏情况时间复杂度接近 O(2^200),且存在大量重复子问题。比如计算到达 (5,5) 的路径数时,可能从多条不同路径重复计算。
思路转折
要想高效 → 必须消除重复计算 → 使用动态规划。
关键观察:如果当前格子是障碍物,路径数为 0;否则路径数 = 左边来的路径数 + 上边来的路径数。注意起点或终点是障碍物时,直接返回 0。
2.2 解法
算法思想
使用二维 DP,定义 dp[i][j] 表示从起点到达位置 (i,j) 的路径数。
递推公式:
if (grid[i-1][j-1] == 1): // 当前是障碍物dp[i][j] = 0else:dp[i][j] = dp[i-1][j] + dp[i][j-1]
边界条件:
-
起点是障碍物 → 直接返回 0
-
dp[1][1] = 1(起点可达时)
i)检查起点 grid[0][0],如果是障碍物直接返回 0
ii)创建 (m+1) × (n+1) 的 dp 数组,使用 1-索引
iii)初始化起点 dp[1][1] = 1
iv)双层循环遍历所有格子:
-
如果是障碍物,dp[i][j] = 0
-
否则累加左边和上边的路径数
v)返回 dp[m][n]
易错点
-
起点障碍物判断:必须在循环前先判断 grid[0][0] == 1,否则会错误初始化
-
索引映射混淆:dp 数组用 1-索引,但原数组是 0-索引。访问原数组时要用 num[i-1][j-1],容易写错
-
障碍物处理时机:判断障碍物要在起点初始化之前,否则起点是障碍物时会错误赋值为 1
-
终点障碍物:如果终点是障碍物,递推过程会自然得到 dp[m][n] = 0,无需额外判断
2.3 代码实现
static class Solution {int[][] dp;int m, n;public int uniquePathsWithObstacles(int[][] num) {m = num.length;n = num[0].length;dp = new int[m + 1][n + 1];// 起点是障碍物,直接返回if (num[0][0] == 1) return 0;dp[1][1] = 1; // 起点初始化for (int i = 1; i <= m; i++) {for (int j = 1; j <= n; j++) {if (i == 1 && j == 1) {continue; // 跳过起点}if (num[i - 1][j - 1] == 1) { // 当前格子是障碍物dp[i][j] = 0; // 到不了,路径数是0} else {dp[i][j] = dp[i - 1][j] + dp[i][j - 1]; // 从上边和左边来}}}return dp[m][n];}
}
复杂度分析
-
时间复杂度:O(m × n),需要遍历整个网格一次
-
空间复杂度:O(m × n),使用了二维 dp 数组