LeetCode #329 Hard

Longest Increasing Path in a Matrix

Longest Increasing Path in a Matrix: find the maximum length of a four-directional path whose values strictly increase.

Constraints
  • m == matrix.length
  • n == matrix[i].length
  • 1 <= m, n <= 200
  • 0 <= matrix[i][j] <= 2³¹ - 1
matrixdfsmemoization
Open on LeetCode ↗
02

Intuition

Longest increasing path in a matrix asks for the longest four-directional path whose values strictly increase. Running a plain DFS from every cell re-explores the same suffixes over and over and is exponential. The property that makes memoisation valid is easy to overlook but essential: because every step must go to a strictly larger value, a path can never return to a cell it has already used. Values would have to decrease at some point to close a loop, and that is forbidden. - The strict increase means the grid is acyclic — an implicit DAG — so no visited-set is needed and results can be cached safely. That matters because it is exactly what separates this from a general graph problem. In a cyclic graph you cannot memoise a DFS result while the search is still in progress; here you can. So define dfs(r, c) as the length of the longest increasing path starting at that cell. Its value is 1 plus the best result among the strictly larger neighbours, or just 1 if no neighbour qualifies. Every cell's answer depends only on cells with larger values, so it is well-defined and computed once. Cache the result on first computation. Each cell is then evaluated once and read many times, and the answer is the maximum over all starting cells. That gives O(rows × cols) — every cell computed once, each doing constant work over its four neighbours.

How to spot this pattern

When moves must strictly increase or decrease, the values themselves impose a DAG even if the grid has undirected adjacency. Memoized DFS is a compact way to compute longest paths in that implicit DAG.

03

Approach

Try it first

Before reading on: price up what the direct approach costs here, then ask which subproblem is being recomputed, and what key identifies it. Aim for O(rows * cols) time and O(rows * cols) space.

1

Notice the grid is acyclic

Strictly increasing moves cannot form a cycle, since returning to a cell would require a decrease. This makes the grid an implicit DAG, which is what licenses caching a DFS result without any visited-set bookkeeping.

2

Define the value per starting cell

dfs(r, c) returns the longest increasing path starting at that cell, counting the cell itself. Starting from a cell rather than ending at one is what makes the recurrence depend only on larger neighbours.

3

Extend only to strictly larger neighbours

Check the four adjacent cells; recurse into any that is in bounds and holds a greater value. The answer is 1 + max(results), or 1 when no neighbour qualifies and the path stops immediately.

4

Memoise on first computation

Store each cell's result in a cache and return it on later visits. Without this the search is exponential — with it, every cell is computed exactly once regardless of how many paths pass through it.

5

Try every cell as a start

The longest path can begin anywhere, so run the memoised DFS from every cell and take the maximum. Later calls are nearly free because most of the grid is already cached.

6

Cost of the memoised search

Each of the rows × cols cells is computed once with constant work over four neighbours, giving O(rows × cols) time and the same for the cache. Recursion depth can reach the number of cells on a monotone grid, which is the one case where an explicit stack helps.

04

Solution & live demo

▶1class Solution:
▶2 def longestIncreasingPath(self, matrix:
▶3 List[List[int]]) -> int:
▶4 rows = len(matrix)
▶5 cols = len(matrix[0])
▶6 
▶7 @cache
▶8 def dfs(row, col):
▶9 best = 1
▶10 for dr, dc in ((1, 0), (-1, 0), (0, 1), (0, -1)):
▶11 nr = row + dr
▶12 nc = col + dc
▶13 if 0 <= nr < rows and 0 <= nc < cols and matrix[nr][nc] > matrix[row][col]:
▶14 best = max(best, 1 + dfs(nr, nc))
▶15 return best
▶16 
▶17 return max(dfs(row, col) for row in range(rows) for col in range(cols))
05

Common pitfalls

Allowing equal-valued moves

✗ Wrong
matrix[nr][nc] >= matrix[row][col]
✓ Right
matrix[nr][nc] > matrix[row][col]

The path must be strictly increasing.

Forgetting to count the current cell

✗ Wrong
best = 0
✓ Right
best = 1

A cell with no larger neighbor still forms a length-one path.

Using one global visited set

✗ Wrong
if (row, col) in visited:
    return 0
✓ Right
@cache
def dfs(row, col):

Cells may belong to many candidate paths; their optimal suffix should be reused, not forbidden.

06

Edge cases

A one-cell matrix

The cell alone forms a path of length one.

All values are equal

Strict comparison permits no move, so every cached length is one.

The best path bends multiple times

Four-directional DFS explores turns without imposing row or column order.

07

Complexity

Time
O(rows * cols)
Space
O(rows * cols)
Memoization computes each cell once and checks four neighbors.