117.Longest Increasing Path in a Matrix
Problem
Given an m x n integer matrix, return the length of the longest strictly increasing path.
From each cell you may move up, down, left or right (no diagonals, no wrapping around the border). Each step must go to a cell with a strictly larger value.
Examples
Input: matrix = [[9,9,4],
[6,6,8],
[2,1,1]]
Output: 4
Explanation: The longest increasing path is 1 → 2 → 6 → 9.Input: matrix = [[3,4,5],
[3,2,6],
[2,2,1]]
Output: 4
Explanation: The longest increasing path is 3 → 4 → 5 → 6.Input: matrix = [[1]]
Output: 1Constraints
m == matrix.length,n == matrix[i].length1 <= m, n <= 2000 <= matrix[i][j] <= 2^31 - 1
💡 Key Insight
Draw an edge from each cell to every strictly larger neighbour. Because values strictly increase along edges, this graph is a DAG — no cycles — so lip(cell) = 1 + max(lip(larger neighbour)) is well defined and has no circular dependencies. Memoizing it makes every cell computed once (O(m·n)). The same DAG can also be processed iteratively with topological sort (peel off local maxima layer by layer), where the number of layers is the answer.
Recursion (Brute Force)
TimeExponential in the worst caseSpaceO(m · n)Intuition
From every starting cell, DFS into each strictly larger neighbour and take the longest result. No visited set is needed — a strictly increasing path can never revisit a cell.
Algorithm
- For each cell
(r, c), computedfs(r, c):best = 1.- For each of the 4 neighbours
(nr, nc)inside the grid withmatrix[nr][nc] > matrix[r][c]:best = max(best, 1 + dfs(nr, nc)).
- Return the maximum over all cells.
Visualization
flowchart TD
A["start at 1 (2,1)"] --> B["2 (2,0)"]
A --> C["6 (1,1)"]
B --> D["6 (1,0)"]
D --> E["9 (0,0)<br/>dead end, len 1"]
C --> F["9 (0,1) len 1"]
C --> G["8 (1,2) len 1"]
E --> H["path 1→2→6→9 = 4"]Starting from different cells recomputes the same sub-paths (e.g. everything above 6) again and again.
Code
class Solution {
private static final int[][] DIRS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
public int longestIncreasingPath(int[][] matrix) {
int best = 0;
for (int r = 0; r < matrix.length; r++) {
for (int c = 0; c < matrix[0].length; c++) {
best = Math.max(best, dfs(matrix, r, c));
}
}
return best;
}
private int dfs(int[][] matrix, int r, int c) {
int best = 1; // the cell itself
for (int[] d : DIRS) {
int nr = r + d[0], nc = c + d[1];
if (nr >= 0 && nr < matrix.length && nc >= 0 && nc < matrix[0].length
&& matrix[nr][nc] > matrix[r][c]) {
best = Math.max(best, 1 + dfs(matrix, nr, nc));
}
}
return best;
}
}Complexity
- Time: Exponential in the worst case — e.g. a snake-like increasing matrix makes each start explore long overlapping paths, up to O(4^(m·n)) branching bounds.
- Space: O(m · n) — recursion depth can equal the path length.
Top-Down Memoization
TimeO(m · n)SpaceO(m · n)Intuition
dfs(r, c) depends only on the cell, never on how we got there (the DAG property). So cache it in memo[r][c]; each cell's answer is computed exactly once.
Recurrence: lip(r, c) = 1 + max(lip(nr, nc)) over neighbours with matrix[nr][nc] > matrix[r][c] (or 1 if none).
Algorithm
memo = new int[m][n](0 = not computed; real answers are ≥ 1).dfs(r, c): returnmemo[r][c]if non-zero; otherwise compute via the recurrence, store, return.- Answer = max of
dfs(r, c)over all cells.
Dry Run
matrix memo (longest path starting here)
9 9 4 1 1 2
6 6 8 2 2 1
2 1 1 3 4 2
memo[2][1] = 1 + max(memo[1][1]=2, memo[2][0]=3) = 4 → answer 4Code
class Solution {
private static final int[][] DIRS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
private int[][] memo;
private int[][] matrix;
private int rows, cols;
public int longestIncreasingPath(int[][] matrix) {
this.matrix = matrix;
rows = matrix.length;
cols = matrix[0].length;
memo = new int[rows][cols];
int best = 0;
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
best = Math.max(best, dfs(r, c));
}
}
return best;
}
private int dfs(int r, int c) {
if (memo[r][c] != 0) {
return memo[r][c]; // already solved
}
int best = 1;
for (int[] d : DIRS) {
int nr = r + d[0], nc = c + d[1];
if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && matrix[nr][nc] > matrix[r][c]) {
best = Math.max(best, 1 + dfs(nr, nc));
}
}
memo[r][c] = best;
return best;
}
}Complexity
- Time: O(m · n) — each cell computed once, 4 neighbours each.
- Space: O(m · n) — memo plus recursion stack (up to
m · ndeep on a snake-shaped path).
Bottom-Up DP by Sorted Value
TimeO(m · n · log(m · n))SpaceO(m · n)Intuition
To tabulate, we need an order where every cell's dependencies (larger neighbours) are already solved. Sorting cells by value descending gives exactly that order. Then dp[r][c] = 1 + max(dp[larger neighbour]).
Algorithm
- Put all cells into a list and sort by value descending.
- For each cell in that order:
dp = 1 + max(dp[nb])over strictly larger neighbours (already processed). - Track the maximum
dp.
Dry Run
order (desc): 9(0,0) 9(0,1) 8(1,2) 6(1,0) 6(1,1) 4(0,2) 2(2,0) 1(2,1) 1(2,2)
9 → 1, 9 → 1, 8 → 1
6(1,0) → 1 + dp[9] = 2 6(1,1) → 1 + max(dp[9], dp[8]) = 2
4(0,2) → 1 + max(dp[9], dp[8]) = 2
2(2,0) → 1 + dp[6] = 3
1(2,1) → 1 + max(dp[6]=2, dp[2]=3) = 4 1(2,2) → 1 + dp[8] = 2
answer = 4Code
import java.util.Arrays;
class Solution {
private static final int[][] DIRS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
public int longestIncreasingPath(int[][] matrix) {
int rows = matrix.length, cols = matrix[0].length;
Integer[] cells = new Integer[rows * cols];
for (int i = 0; i < cells.length; i++) {
cells[i] = i; // encode (r, c) as r * cols + c
}
// largest values first, so larger neighbours are solved before smaller ones
Arrays.sort(cells, (a, b) -> Integer.compare(matrix[b / cols][b % cols], matrix[a / cols][a % cols]));
int[][] dp = new int[rows][cols];
int best = 0;
for (int id : cells) {
int r = id / cols, c = id % cols;
dp[r][c] = 1;
for (int[] d : DIRS) {
int nr = r + d[0], nc = c + d[1];
if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && matrix[nr][nc] > matrix[r][c]) {
dp[r][c] = Math.max(dp[r][c], 1 + dp[nr][nc]);
}
}
best = Math.max(best, dp[r][c]);
}
return best;
}
}Complexity
- Time: O(m · n · log(m · n)) — dominated by the sort.
- Space: O(m · n) — the cell list and
dp.
Topological Sort / Kahn's BFSOptimal
TimeO(m · n)SpaceO(m · n)Intuition
Avoid both the sort and deep recursion. For each cell count its out-degree = number of strictly larger neighbours. Cells with out-degree 0 are local maxima — paths end there. Remove them as layer 1; this lowers the out-degree of their smaller neighbours, some of which hit 0 and form layer 2, and so on. The longest path visits one cell per layer, so the number of layers is the answer.
Algorithm
- Compute
outDeg[r][c]for every cell; enqueue all cells withoutDeg == 0. layers = 0. While the queue is non-empty:layers++; process exactly the current queue size.- For each popped cell, for each smaller neighbour: decrement its
outDeg; if it becomes0, enqueue it.
- Return
layers.
Dry Run
flowchart LR
L1["Layer 1<br/>9 9 8<br/>(local maxima)"] --> L2["Layer 2<br/>6 6 4 1(2,2)"]
L2 --> L3["Layer 3<br/>2 (2,0)"]
L3 --> L4["Layer 4<br/>1 (2,1)"]
L4 --> R["4 layers → answer 4"]1 (2,1) waits until layer 4 because one of its larger neighbours, 2 (2,0), is only removed in layer 3.
Code
import java.util.ArrayDeque;
import java.util.Deque;
class Solution {
private static final int[][] DIRS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
public int longestIncreasingPath(int[][] matrix) {
int rows = matrix.length, cols = matrix[0].length;
int[][] outDeg = new int[rows][cols];
Deque<int[]> queue = new ArrayDeque<>();
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
for (int[] d : DIRS) {
int nr = r + d[0], nc = c + d[1];
if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && matrix[nr][nc] > matrix[r][c]) {
outDeg[r][c]++; // edge to a larger neighbour
}
}
if (outDeg[r][c] == 0) {
queue.offer(new int[]{r, c}); // local maximum
}
}
}
int layers = 0;
while (!queue.isEmpty()) {
layers++;
for (int size = queue.size(); size > 0; size--) {
int[] cell = queue.poll();
int r = cell[0], c = cell[1];
for (int[] d : DIRS) {
int nr = r + d[0], nc = c + d[1];
// a smaller neighbour loses one outgoing edge
if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && matrix[nr][nc] < matrix[r][c]) {
if (--outDeg[nr][nc] == 0) {
queue.offer(new int[]{nr, nc});
}
}
}
}
}
return layers;
}
}Complexity
- Time: O(m · n) — every cell enters the queue once; 4 neighbours each.
- Space: O(m · n) — out-degree table and queue; no recursion stack.
Complexity Comparison
| Approach | Time | Space |
|---|---|---|
| Recursion (Brute Force) | Exponential | O(m · n) |
| Top-Down Memoization | O(m · n) | O(m · n) |
| Bottom-Up DP by Sorted Value | O(m · n · log(m · n)) | O(m · n) |
| Topological Sort (Kahn's BFS) | O(m · n) | O(m · n) |
Common Pitfalls
- Adding a
visitedset during DFS — unnecessary (strict increase prevents cycles) and it breaks memoization if you reset it wrongly. - Using
>=instead of>— equal neighbours are not allowed and would create cycles. - Using
0as both "unvisited" and a real answer — fine here only because every real answer is at least1. - Deep recursion on a 200 x 200 snake-shaped matrix can approach 40,000 frames; the BFS version avoids stack overflow risk.
- In Kahn's algorithm, counting nodes processed instead of layers — the answer is the number of BFS levels.