329. 矩阵中的最长递增路径
- LeetCode:329. 矩阵中的最长递增路径 · LCR 112. 最长递增路径
- 难度:困难
- 归类:图、记忆化搜索、深度优先搜索、动态规划
- 主解法:记忆化 DFS,枚举每个起点并缓存已计算结果
先给结论
把矩阵中的每个单元格看成图的一个节点,相邻且值更大的单元格之间有向边相连。由于数值严格递增,图中不可能存在环。
定义:
对每个单元格向四个更大的邻居递归搜索,取最大值再加 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]。 - 当前格的结果等于所有合法邻居结果的最大值加 1(加 1 是包含自己)。
如果没有更大的邻居,就只能停在当前格,memo[row][col] = 1。
为什么不需要 visited
因为每次只能移动到值严格更大的单元格,数值一路递增,不可能沿任何路径走回已经访问过的单元格。严格递增天然保证了图是有向无环图(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。
JavaScript 实现
代码与思路对照
正确性说明
- 无环保证正确遍历。 每次只能移动到值严格更大的单元格,数值不可能无限递增后回到起点,因此图中不存在环,DFS 不会陷入死循环。
- 状态定义正确。
memo[row][col]缓存的是从该单元格出发、沿递增方向能走出的最长路径长度,与问题所求完全一致。 - 状态转移正确。 从当前格出发,下一步可以走向任意更大的合法邻居;最长的后续路径就是这些邻居缓存值的最大值,再加上当前这一步,即
1 + max(...)。 - 枚举起点正确。 最长递增路径的起点可以是任意单元格,因此遍历整个矩阵、对每个单元格调用
dfs取最大值,一定能覆盖全局最优解。
边界与陷阱
- 单个单元格:
dfs不会找到更大邻居,maxLen保持为1,返回1。 - 所有值相等: 没有任何邻居满足
>,所有memo值都是1,答案为1。 - 严格大于: 转移条件必须是
>,不能使用>=。相等值之间不能移动。 - 四个方向: 不能遗漏上或左,也不能允许对角线。
- memo 初始值: 题目数值范围是
0到2^31 - 1,最小路径长度是1,所以用0作为"未计算"标记是安全的。 - 不修改原矩阵: 当前实现只读取
matrix,不修改它。
复杂度分析
设矩阵大小为 m × n:
- 时间复杂度:
O(mn)。每个单元格只会被计算一次,每次计算最多检查四个邻居。 - 额外空间复杂度:
O(mn)。memo数组占用mn个整数;递归栈深度最多为最长递增路径长度,不超过mn。
与拓扑排序 BFS 的取舍
这道题也可以用拓扑排序(Kahn 算法)求解:把递增关系建成 DAG,统计入度,从局部最小值开始逐层 BFS,层数即为最长路径长度。
结论: 记忆化 DFS 是这道题的标准解法,代码简洁、思路直观,适合面试快速实现。只有在特别担心递归深度(如题目规模极大)时才考虑拓扑 BFS。
面试官递进追问
1. 为什么严格递增关系保证不会出现环?
沿任意合法路径移动,数值严格增加。如果存在环,沿环走一圈后起点值必须严格大于自身,矛盾。
2. 为什么不需要 visited 数组?
因为数值只能增大不能减小,不可能从当前路径走回已经访问过的单元格。天然的有向无环结构替代了显式的访问标记。
3. 如果不加 memo 缓存,时间复杂度是多少?
退化为普通 DFS,每个起点独立搜索,大量子问题重复计算,最坏时间复杂度为指数级 O(2^(mn))。
4. memo 用 0 作为未计算标记是否安全?
安全。题目要求的是路径长度,最短路径至少包含当前单元格本身,即长度至少为 1。0 不可能是一个合法的路径长度结果。
5. 能不能把问题改成"求以每个单元格结尾的最长递增路径"?
可以。只需把 DFS 中的比较方向从 > 改成 <,即只走向更小的邻居,状态定义相应改为"以当前格结尾的最长递减路径"。两种定义本质相同。
6. 时间复杂度为什么是 O(mn)?
每个单元格作为子问题只会被计算一次(首次调用 dfs 时写入 memo,后续直接返回)。单次计算最多遍历四个方向,是 O(1)。总共 mn 个单元格,因此总时间为 O(mn)。
常见错误
- 把矩阵当作普通二维 DP,却没有确定状态依赖的计算顺序(DAG 上的 DP 天然适合记忆化搜索)。
- 只从某一个单元格开始搜索,遗漏其他可能的起点。
- 允许移动到相等值,破坏严格递增条件。
- 使用对角线移动,或遗漏上、左方向。
- 忘记加
1(maxLen初始应为1,代表只包含当前单元格)。 - 不加
memo缓存,导致超时。 - 把
memo初始值设为非零值,与合法结果混淆。
可迁移总结
- DAG 上的 DP: 有向无环图中的最长路径问题,常用记忆化 DFS 或拓扑排序求解。
- 网格转图: 单元格是节点,合法移动是边;单调关系(严格递增/递减)天然形成 DAG。
- 记忆化搜索: 自顶向下带缓存的递归,代码直观,状态转移公式清晰。
- 状态设计:
memo[i][j]表示从某点出发/到达某点的最优值,根据题目要求选择方向。 - 一句话记忆: 向更大的邻居递归,缓存每个格子的最优结果,全局取最大。
刷题后自测
先只回答第 1 题,再展开后续问题:
- 为什么从每个单元格出发都要调用一次
dfs,而不是只从一个起点开始?
完成第 1 题后再看第 2 题
- 如果把
matrix[nextRow][nextCol] > matrix[row][col]改成>=,会发生什么?
完成前两题后再看第 3 题
- 什么情况下递归栈深度最大?最大可能是多少?
完成前三题后再看第 4 题
- 写出拓扑排序 BFS 的核心思路,并说明它与记忆化 DFS 的时间复杂度关系。

