矩阵中的最长递增路径 图解题解
这道题到底在问什么
- 输入
- matrix=[[9,9,4],[6,6,8],[2,1,1]]
- 输出
- 4
最优解:为什么这么做
一句话答案:LeetCode 329 矩阵中的最长递增路径:每格只能走向更大的邻居,用记忆化 DFS,dp[i][j]=1+四邻中更大格的最大 dp,严格递增保证无环所以每格只算一次,时间 O(R·C)、空间 O(R·C)。
最长递增路径,是在矩阵里找什么
给一个数字矩阵 matrix,从任意一格出发,每步只能上下左右走到值严格更大的相邻格,问最长这样一条路径有几个格。题面 matrix=[[9,9,4],[6,6,8],[2,1,1]],答案是 4,对应 1→2→6→9 那条,起点终点都不限。
四向随便递归,为什么同一格会被反复走
不做记忆的话,从每一格都发起一次深度优先搜索(DFS,顺一条路探到走不动再回头),四邻里谁更大就往谁递归。问题是同一格被反复重算:值 6 那格往上能走多远,从值 2、值 1 的格各问一遍,都从零往下探。矩阵一大,每格被无数条路重复展开,时间指数膨胀,病根是没把「这格能走出多长」记下来。
每格能走多远,为什么只看更大的邻居
这道题用动态规划解:把每格「能走多远」这个小问题算一次、存好,用到时取。定义 dp[i][j](i 是行号、j 是列号,都从 0 数起)表示从这格出发能走出的最长递增路径有几个格。每步只能走向更大的格,那下一步只能进四邻里比它大的格,它们还能走多远正是它们的 dp。于是 dp[i][j] = 1 + 四邻里比它大的格的 dp 的最大值,1 是这格自己;四邻都不比它大就是终点,dp 为 1。答案是整张 dp 表的最大值。
严格递增凭什么保证记忆化不会绕死
能把算过的 dp 存下来复用,全靠「严格更大」这三个字。dp[i][j] 只依赖比它值更大的邻居,路径上的值一路严格变大,不会绕一圈回到起点——回去就得踩一个不更大的格,规则不许。没有环,就不会陷入「dp 互相依赖、谁都算不出」的死循环,每格的 dp 靠一批「值更大、已算好」的格就能算出来。于是给每格加个备忘(这就是记忆化:算过一次就存进 memo 表,下次撞到直接取、不再递归),第二次遇到就秒返回。
拿题面这个 3×3 矩阵把 memo 填一遍
按值从大到小填最顺。最大的几格 (0,0)、(0,1) 值 9 和 (1,2) 值 8 四邻都不更大,是终点,memo 记 1。值 6 的 (1,0) 接上更大邻居 (0,0) 的 memo 1,得 2;值 2 的 (2,0) 再接 (1,0) 的 memo 2,得 3。轮到值 1 的 (2,1),更大邻居里 (2,0) 的 memo 3 最长,接上加自己得 memo[2][1]=4——正是路径 1→2→6→9。其余格同理填好(如 (1,1)、(0,2) 都是 2),九格填完最大 memo 是 4,落在 (2,1),就是答案。
看着是四向递归,为什么复杂度只有 O(R·C)
记忆化把重复挡在外面。设矩阵 R 行 C 列,每格的 dp 只在第一次访问时真正算一次,之后碰到直接取 memo,计算量就是格子数乘常数,时间 O(R·C);memo 表加递归栈占 O(R·C) 空间。
严格大小别写松,写成「大于等于」就会在两个相等的格之间来回跳、绕出环,递归停不下来(题面那排 9、那两个 1 都是等值,最易撞上)。空矩阵也得先挡,matrix 一行都没有就返回 0,不然取 len(matrix[0]) 会出错。答案还得扫遍整张 dp 取最大,只认某一格会漏掉最长的那条。
▶ 动画逐步走查(共 22 步)——想跟着动画一帧帧对照就展开
- 3关键:每格的答案只看「更大的邻居」。所以先把大值格算完,小值格直接拿现成结果——这就是记忆化,避免重复递归。
- 4上半三行是矩阵原始值(始终不变),下半三行 memo 待填。
- 5蓝色是输入矩阵。按值从大到小处理:先算大值格(它没有更大邻居 = 1),小值格再来接现成的大邻居结果。
- 6当前格值 9:上下左右都不比它大,是路径终点,自己算 1 步。
- 7填入 memo[0][0]=1(终点格,就 1 步)。
- 8当前格值 9:上下左右都不比它大,是路径终点,自己算 1 步。
- 9填入 memo[0][1]=1(终点格,就 1 步)。
- 10当前格值 8:上下左右都不比它大,是路径终点,自己算 1 步。
- 11填入 memo[1][2]=1(终点格,就 1 步)。
- 12当前格值 6:更大邻居有 (0,0)值9→memo 1。接最长的那个,再加自己这一步。
- 13填入 memo[1][0]=2(沿着 (0,0) 那条最长,再 +1)。
- 14当前格值 6:更大邻居有 (0,1)值9→memo 1、(1,2)值8→memo 1。接最长的那个,再加自己这一步。
- 15填入 memo[1][1]=2(沿着 (0,1) 那条最长,再 +1)。
- 16当前格值 4:更大邻居有 (1,2)值8→memo 1、(0,1)值9→memo 1。接最长的那个,再加自己这一步。
- 17填入 memo[0][2]=2(沿着 (1,2) 那条最长,再 +1)。
- 18当前格值 2:更大邻居有 (1,0)值6→memo 2。接最长的那个,再加自己这一步。
- 19填入 memo[2][0]=3(沿着 (1,0) 那条最长,再 +1)。
- 20当前格值 1:更大邻居有 (1,1)值6→memo 2、(2,0)值2→memo 3。接最长的那个,再加自己这一步。
- 21填入 memo[2][1]=4(沿着 (2,0) 那条最长,再 +1)。
- 22当前格值 1:更大邻居有 (1,2)值8→memo 1。接最长的那个,再加自己这一步。
- 23填入 memo[2][2]=2(沿着 (1,2) 那条最长,再 +1)。
- 24所有格算完,最大的 memo = 4(从值 1 出发那格),就是全矩阵最长递增路径的长度。
⚠️ 容易写错的地方
✗ 错:允许走相等的格
✓ 对:必须严格更大
相等会绕圈、路径无限长
✗ 错:不缓存、纯暴力 DFS
✓ 对:加 memo
否则指数级重复递归会超时
✗ 错:memo 用 0 当未算又当合法值
✓ 对:路径长度最小是 1,0 可安全表示未算
避免缓存判断踩坑
完整代码(Python / C++ / Java)
Python
def longestIncreasingPath(matrix):
R, C = len(matrix), len(matrix[0])
from functools import lru_cache
@lru_cache(None)
def dfs(i, j):
best = 1
for di, dj in ((-1,0),(1,0),(0,-1),(0,1)):
ni, nj = i+di, j+dj
if 0<=ni<R and 0<=nj<C and matrix[ni][nj]>matrix[i][j]:
best = max(best, 1 + dfs(ni, nj))
return best
return max(dfs(i, j) for i in range(R) for j in range(C))C++
int R, C;
int dfs(vector<vector<int>>& m, vector<vector<int>>& memo, int i, int j){
if(memo[i][j]) return memo[i][j];
int best = 1, di[4]={-1,1,0,0}, dj[4]={0,0,-1,1};
for(int k=0;k<4;k++){
int ni=i+di[k], nj=j+dj[k];
if(ni>=0&&ni<R&&nj>=0&&nj<C&&m[ni][nj]>m[i][j])
best = max(best, 1 + dfs(m, memo, ni, nj));
}
return memo[i][j] = best;
}
int longestIncreasingPath(vector<vector<int>>& m){
R=m.size(); C=m[0].size();
vector<vector<int>> memo(R, vector<int>(C,0));
int ans=0;
for(int i=0;i<R;i++) for(int j=0;j<C;j++) ans=max(ans, dfs(m,memo,i,j));
return ans;
}Java
class Solution {
int R, C;
int[][] memo;
int[] di = {-1, 1, 0, 0}, dj = {0, 0, -1, 1};
public int longestIncreasingPath(int[][] matrix) {
R = matrix.length; C = matrix[0].length;
memo = new int[R][C];
int ans = 0;
for (int i = 0; i < R; i++)
for (int j = 0; j < C; j++)
ans = Math.max(ans, dfs(matrix, i, j));
return ans;
}
int dfs(int[][] m, int i, int j) {
if (memo[i][j] != 0) return memo[i][j];
int best = 1;
for (int k = 0; k < 4; k++) {
int ni = i + di[k], nj = j + dj[k];
if (ni >= 0 && ni < R && nj >= 0 && nj < C && m[ni][nj] > m[i][j])
best = Math.max(best, 1 + dfs(m, ni, nj));
}
return memo[i][j] = best;
}
}复杂度
时间
O(R·C)
每格只真正算一次,之后命中缓存
空间
O(R·C)
memo 表 + 递归栈
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 矩阵中的最长递增路径 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
记忆化 DFS 和自底向上 DP 是一回事吗,能不能不用递归?+
本质一样,都靠「每格 dp 只依赖值更大的邻居」这条无环性质。记忆化 DFS 是自顶向下、按需递归、算过缓存;自底向上则先把所有格按值从大到小排序,再依次算 dp——值大的先算好,轮到小的时它要的邻居 dp 全就绪。两者时间都是 O(R·C),把「按值从大到小填表」跑一遍,结果和记忆化递归完全一致。
为什么答案要扫整张 dp 表取最大,不能从某个固定格出发?+
因为最长递增路径可以从任意一格起头,事先并不知道哪格是最优起点。dp[i][j] 只算了「从这格出发」的最长长度,不同起点长短不一:题面里最长那条从值 1 的格起头、长度 4,而从值 9 的格起头只有 1。所以必须把每格都当一次起点算出 dp,再取全表最大,才不会漏掉真正最长的路径。
如果把「严格更大」改成「大于等于」(允许走向相等的格)会怎样?+
会出问题。相等的格之间可以互相走,路径的值不再严格变大,就可能绕成环——A 走到等值的 B、B 又走回 A,记忆化依赖的无环前提被打破,dp[A] 要 dp[B]、dp[B] 又要 dp[A],递归陷入死循环。严格递增正是挡住环、让记忆化成立的关键;真要允许相等,得换别的模型(如按连通块处理),不能直接套这套 dp。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 矩阵中的最长递增路径 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。