把矩阵中的每个单元格看成图的一个节点,相邻且值更大的单元格之间有向边相连。由于数值严格递增,图中不可能存在环。
定义:
对每个单元格向四个更大的邻居递归搜索,取最大值再加 1。用 memo 数组缓存结果,避免重复计算。
最后遍历所有单元格,取 memo 中的最大值即为答案。
给定一个 m × n 整数矩阵 matrix,返回其中最长严格递增路径的长度。
从每个单元格只能向上、下、左、右移动:
示例 1:
示例 2:
示例 3:
题目约束:
1 <= m, n <= 200。0 <= matrix[row][col] <= 2^31 - 1。每个单元格都可以作为路径起点,因此最终答案是所有 memo[row][col] 的最大值。
从 (row, col) 出发,检查上下左右四个邻居:
memo[neighbor]。如果没有更大的邻居,就只能停在当前格,memo[row][col] = 1。
因为每次只能移动到值严格更大的单元格,数值一路递增,不可能沿任何路径走回已经访问过的单元格。严格递增天然保证了图是有向无环图(DAG),所以不需要额外的 visited 集合来防止环。
如果不缓存,从多个不同的起点搜索时,会重复计算同一个单元格的结果。例如示例 1 中,6 既可以从 2 走到,也可以从另一个 1 走到。记忆化确保每个单元格只递归计算一次,之后直接查表返回。
memo 矩阵,所有值设为 0,表示尚未计算。(r, c) 调用 dfs(r, c):
memo[r][c] !== 0,直接返回缓存值。dfs,收集最大值。memo[r][c] = 1 + max,返回该值。ans。ans。对于:
从每个单元格出发的最长递增路径长度(memo 值):
(2,1)=1 没有更小的邻居(题目是递增,所以从 1 出发),更大的邻居是 (2,0)=2,所以 memo[2][1] = 1 + memo[2][0] = 1 + 3 = 4?等等,这里方向是只能走向更大的值。让我重新推演:
从 (2,1)=1 出发:
(2,0)=2(更大)(1,1)=6(更大)从 (2,0)=2 出发:
(1,0)=6(更大)从 (1,0)=6 出发:
(0,0)=9(更大)(0,1)=9(更大)从 (0,0)=9 出发:没有更大的邻居 → memo[0][0] = 1
所以 memo[1][0] = 1 + max(memo[0][0], memo[0][1]) = 1 + 1 = 2
memo[2][0] = 1 + memo[1][0] = 3
从 (2,1)=1 出发:
(2,0)=2 的路径长度 = 1 + memo[2][0] = 4(1,1)=6 的路径长度 = 1 + memo[1][1]从 (1,1)=6 出发:
(0,1)=9(1,2)=8(0,1)=9 没有更大的邻居 → memo[0][1] = 1(1,2)=8 可以走到 (0,2)=4?不,4 < 8,不能走。可以走到 (0,2)=4?不行。(0,2)=4 更小。那 (1,2)=8 有更大的邻居吗?(0,2)=4 更小,(1,1)=6 更小。没有更大的。所以 memo[1][2] = 1。所以 memo[1][1] = 1 + max(memo[0][1], memo[1][2]) = 1 + 1 = 2
memo[2][1] = 1 + max(memo[2][0], memo[1][1]) = 1 + max(3, 2) = 4
最大值为 4,对应路径 1 → 2 → 6 → 9。
让我写出正确的 memo 矩阵:
等等,(2,2)=1:可以走到 (1,2)=8,所以 memo[2][2] = 1 + memo[1][2] = 1 + 1 = 2?
不对,(2,2)=1 的邻居:
(1,2)=8 > 1,可以走(2,1)=1 = 1,不能走(严格递增)
所以 memo[2][2] = 1 + memo[1][2] = 2(0,2)=4:邻居 (1,2)=8 > 4,所以 memo[0][2] = 1 + memo[1][2] = 2
修正后的 memo:
最大值是 4。
好的,我会在文档中正确写出这个推演。
参考实现来源:JoshCrozier/leetcode-javascript,原项目采用 MIT License。
| 阶段 | 对应代码 | 作用 |
|---|---|---|
| 边界处理 | 空矩阵返回 0 | 兼容题目约束之外的空输入 |
| 初始化 memo | Array(cols).fill(0) | 0 表示尚未计算 |
| 命中缓存 | memo[row][col] !== 0 | 避免重复递归同一单元格 |
| 枚举方向 | directions 四方向偏移 | 上下左右移动 |
| 合法性判断 | 越界检查 + > 严格递增 | 只走向值更大的邻居 |
| 状态转移 | 1 + dfs(nextRow, nextCol) | 走一步,加上后续最优结果 |
| 缓存结果 | memo[row][col] = maxLen | 当前单元格的最优解只算一次 |
| 全局最大 | ans = Math.max(ans, dfs(...)) | 每个单元格都可能是全局最长路径的起点 |
memo[row][col] 缓存的是从该单元格出发、沿递增方向能走出的最长路径长度,与问题所求完全一致。1 + max(...)。dfs 取最大值,一定能覆盖全局最优解。dfs 不会找到更大邻居,maxLen 保持为 1,返回 1。>,所有 memo 值都是 1,答案为 1。>,不能使用 >=。相等值之间不能移动。0 到 2^31 - 1,最小路径长度是 1,所以用 0 作为"未计算"标记是安全的。matrix,不修改它。设矩阵大小为 m × n:
O(mn)。每个单元格只会被计算一次,每次计算最多检查四个邻居。O(mn)。memo 数组占用 mn 个整数;递归栈深度最多为最长递增路径长度,不超过 mn。这道题也可以用拓扑排序(Kahn 算法)求解:把递增关系建成 DAG,统计入度,从局部最小值开始逐层 BFS,层数即为最长路径长度。
| 记忆化 DFS | 拓扑排序 BFS | |
|---|---|---|
| 代码量 | 更少,逻辑更直接 | 更多,需要建图和统计入度 |
| 时间复杂度 | O(mn) | O(mn) |
| 空间复杂度 | O(mn)(memo + 递归栈) | O(mn)(入度数组 + 队列) |
| 风险 | 极端路径长度可达 mn(最大 40,000),可能触发 JavaScript 调用栈溢出 | 纯迭代,无栈溢出风险 |
结论: 记忆化 DFS 是这道题的标准解法,代码简洁、思路直观,适合面试快速实现。只有在特别担心递归深度(如题目规模极大)时才考虑拓扑 BFS。
沿任意合法路径移动,数值严格增加。如果存在环,沿环走一圈后起点值必须严格大于自身,矛盾。
visited 数组?因为数值只能增大不能减小,不可能从当前路径走回已经访问过的单元格。天然的有向无环结构替代了显式的访问标记。
memo 缓存,时间复杂度是多少?退化为普通 DFS,每个起点独立搜索,大量子问题重复计算,最坏时间复杂度为指数级 O(2^(mn))。
memo 用0 作为未计算标记是否安全?安全。题目要求的是路径长度,最短路径至少包含当前单元格本身,即长度至少为 1。0 不可能是一个合法的路径长度结果。
可以。只需把 DFS 中的比较方向从 > 改成 <,即只走向更小的邻居,状态定义相应改为"以当前格结尾的最长递减路径"。两种定义本质相同。
O(mn)?每个单元格作为子问题只会被计算一次(首次调用 dfs 时写入 memo,后续直接返回)。单次计算最多遍历四个方向,是 O(1)。总共 mn 个单元格,因此总时间为 O(mn)。
1(maxLen 初始应为 1,代表只包含当前单元格)。memo 缓存,导致超时。memo 初始值设为非零值,与合法结果混淆。memo[i][j] 表示从某点出发/到达某点的最优值,根据题目要求选择方向。先只回答第 1 题,再展开后续问题:
dfs,而不是只从一个起点开始?matrix[nextRow][nextCol] > matrix[row][col] 改成 >=,会发生什么?