329. Longest Increasing Path in a Matrix
Problem Description
You are given an m x n
matrix of integers. Your task is to find the length of the longest increasing path in this matrix.
From any cell in the matrix, you can move in four directions: left, right, up, or down (no diagonal moves allowed). You cannot move outside the matrix boundaries, and wrap-around is not permitted.
An increasing path means that each subsequent cell you move to must have a strictly greater value than the current cell. For example, if you're at a cell with value 5
, you can only move to adjacent cells with values greater than 5
.
The goal is to find the maximum length among all possible increasing paths that can be formed starting from any cell in the matrix.
For instance, if you have a matrix:
9 9 4 6 6 8 2 1 1
Starting from the cell with value 1
(bottom row), you could form the path: 1 β 2 β 6 β 9
, which has length 4
. This would be the longest increasing path in this matrix.
The challenge involves exploring all possible starting positions and all possible paths from each position to determine which path achieves the maximum length.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: We can model this problem as a graph where each cell in the matrix is a node, and edges exist between adjacent cells when we can move from one to another (i.e., when the destination cell has a greater value).
Is it a tree?
- No: The graph representation is not a tree because it can have multiple paths between nodes and may contain cycles when considering the undirected version. However, when we only move to cells with strictly greater values, we create a directed acyclic graph (DAG).
Is the problem related to directed acyclic graphs?
- Yes: Since we can only move from a cell to an adjacent cell with a strictly greater value, we're essentially creating a DAG. No cycles can exist because we always move to strictly increasing values.
Does the problem involve topological sort?
- Not directly, but the problem is related to finding the longest path in a DAG, which can be solved using DFS with memoization.
Alternative path through small constraints:
- The problem also fits the pattern of having manageable constraints (matrix size up to reasonable bounds), and we need to explore all possible paths from each cell.
Does the problem have small constraints?
- Yes: The matrix dimensions are bounded, making it feasible to explore all cells.
DFS/backtracking?
- Yes: We need DFS to explore all possible increasing paths from each starting position, combined with memoization to avoid recomputing paths.
Conclusion: The flowchart suggests using DFS (Depth-First Search) with memoization. This makes sense because we need to explore all possible increasing paths from each cell in the matrix, and DFS allows us to recursively traverse these paths while memoization prevents redundant calculations.
Intuition
The key insight is recognizing that this problem is about finding paths in a directed acyclic graph (DAG). Since we can only move to cells with strictly greater values, it's impossible to return to a previously visited cell in the same path - this naturally prevents cycles.
Think of each cell as a starting point for potential paths. From any cell (i, j)
, we want to know: "What's the longest increasing path I can build starting from here?" This question has an optimal substructure property - if we know the longest path from each of our neighbors, we can determine the longest path from our current position.
Consider a cell with value 5
. If it has adjacent cells with values 7
, 8
, and 3
, we can only move to cells with 7
and 8
. If the longest path from the cell with 7
is length 3
and from the cell with 8
is length 2
, then the longest path from our current cell would be 1 + max(3, 2) = 4
(including the current cell itself).
This recursive relationship naturally leads to a DFS solution. However, naive DFS would repeatedly calculate the same paths. For instance, multiple cells might lead to the same cell, causing us to recalculate that cell's longest path multiple times. This is where memoization becomes crucial - once we've computed the longest path from a cell, we store it and reuse it whenever needed.
The beauty of this approach is that we're essentially performing dynamic programming on a graph structure. Each cell's result depends only on its neighbors with greater values, creating a dependency chain that we can solve recursively. By trying all possible starting positions and keeping track of the maximum, we find the overall longest increasing path in the matrix.
The @cache
decorator (or memoization) transforms what would be an exponential time complexity problem into a linear one, as each cell is computed exactly once despite potentially being part of many different paths.
Solution Approach
The solution implements a memoized depth-first search (DFS) approach to find the longest increasing path. Let's walk through the implementation step by step:
Core Function Design:
We define a recursive function dfs(i, j)
that computes the length of the longest increasing path starting from position (i, j)
. The @cache
decorator automatically memoizes the results, ensuring each cell is computed only once.
DFS Logic:
For each cell (i, j)
, the function:
- Initializes
ans = 0
to track the maximum path length from neighboring cells - Explores all four directions (up, down, left, right) using the clever
pairwise((-1, 0, 1, 0, -1))
iteration - For each valid neighbor
(x, y)
that satisfies:- Within bounds:
0 <= x < m
and0 <= y < n
- Has a greater value:
matrix[x][y] > matrix[i][j]
- Within bounds:
- Recursively calls
dfs(x, y)
and updatesans
with the maximum path length found - Returns
ans + 1
(adding 1 for the current cell itself)
Direction Traversal Technique:
The code uses pairwise((-1, 0, 1, 0, -1))
to generate direction pairs:
(-1, 0)
: move up(0, 1)
: move right(1, 0)
: move down(0, -1)
: move left
This compact representation avoids explicitly listing all four direction vectors.
Main Function: The main function:
- Extracts matrix dimensions
m
andn
- Calls
dfs(i, j)
for every cell in the matrix - Returns the maximum value among all starting positions
Why This Works:
- The memoization ensures
O(m Γ n)
time complexity since each cell is computed exactly once - The recursive structure naturally handles the dependency relationships - a cell's longest path depends on its valid neighbors' longest paths
- By trying all starting positions, we guarantee finding the global maximum
Space Complexity:
The solution uses O(m Γ n)
space for:
- The memoization cache storing results for each cell
- The recursion stack (at most
O(m Γ n)
in the worst case of a strictly increasing spiral)
This approach elegantly combines DFS traversal with dynamic programming principles through memoization, making it both intuitive and efficient.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's trace through the algorithm with a small 3Γ3 matrix:
Matrix: [1, 2, 3] [6, 5, 4] [7, 8, 9]
Step 1: Initialize and Start DFS from Each Cell
We'll call dfs(i, j)
for every cell. Let's trace a few key calls to understand the process.
Step 2: DFS from cell (0, 0) with value 1
- Current value: 1
- Check all 4 neighbors:
- Up (-1, 0): Out of bounds, skip
- Right (0, 1): Value = 2 > 1 β Call
dfs(0, 1)
- Down (1, 0): Value = 6 > 1 β Call
dfs(1, 0)
- Left (0, -1): Out of bounds, skip
- Need to compute
dfs(0, 1)
anddfs(1, 0)
first
Step 3: DFS from cell (0, 1) with value 2
- Current value: 2
- Check neighbors:
- Up: Out of bounds
- Right (0, 2): Value = 3 > 2 β Call
dfs(0, 2)
- Down (1, 1): Value = 5 > 2 β Call
dfs(1, 1)
- Left (0, 0): Value = 1 < 2 β Skip
Step 4: DFS from cell (0, 2) with value 3
- Current value: 3
- Check neighbors:
- Right: Out of bounds
- Down (1, 2): Value = 4 > 3 β Call
dfs(1, 2)
- Others don't satisfy the increasing condition
dfs(1, 2)
continues the chain...
Step 5: Building the Path
Following the complete recursion, we discover the longest path:
- Start at (0, 0): value = 1
- Move to (0, 1): value = 2
- Move to (0, 2): value = 3
- Move to (1, 2): value = 4
- Move to (1, 1): value = 5
- Move to (1, 0): value = 6
- Move to (2, 0): value = 7
- Move to (2, 1): value = 8
- Move to (2, 2): value = 9
Step 6: Memoization in Action
When we later call dfs(1, 1)
from another starting point:
- The value is already cached as 5 (path: 5β6β7β8β9)
- We immediately return the cached value without recomputation
Step 7: Final Result
After checking all starting positions:
dfs(0, 0)
returns 9 (the complete path shown above)dfs(2, 2)
returns 1 (no valid moves from value 9)- Other cells return intermediate values
The maximum among all starting points is 9.
Key Observations:
- Each cell is computed only once due to memoization
- The recursion naturally follows increasing values, preventing cycles
- The algorithm explores all possible paths efficiently by reusing computed results
Solution Implementation
1class Solution:
2 def longestIncreasingPath(self, matrix: List[List[int]]) -> int:
3 from functools import cache
4
5 @cache
6 def dfs(row: int, col: int) -> int:
7 """
8 Perform depth-first search from position (row, col) to find
9 the longest increasing path starting from this cell.
10
11 Args:
12 row: Current row index
13 col: Current column index
14
15 Returns:
16 Length of the longest increasing path from this cell
17 """
18 # Initialize the maximum path length from neighboring cells
19 max_path_length = 0
20
21 # Check all four directions: up, right, down, left
22 directions = [(-1, 0), (0, 1), (1, 0), (0, -1)]
23
24 for delta_row, delta_col in directions:
25 # Calculate neighboring cell coordinates
26 next_row = row + delta_row
27 next_col = col + delta_col
28
29 # Check if the neighbor is within bounds and has a greater value
30 if (0 <= next_row < rows and
31 0 <= next_col < cols and
32 matrix[next_row][next_col] > matrix[row][col]):
33 # Recursively find the longest path from the neighbor
34 max_path_length = max(max_path_length, dfs(next_row, next_col))
35
36 # Return the longest path including the current cell (+1)
37 return max_path_length + 1
38
39 # Get matrix dimensions
40 rows = len(matrix)
41 cols = len(matrix[0])
42
43 # Find the maximum path length starting from each cell
44 max_path = 0
45 for i in range(rows):
46 for j in range(cols):
47 max_path = max(max_path, dfs(i, j))
48
49 return max_path
50
1class Solution {
2 // Dimensions of the matrix
3 private int rows;
4 private int cols;
5
6 // Input matrix
7 private int[][] matrix;
8
9 // Memoization table to store the longest path starting from each cell
10 private int[][] memo;
11
12 /**
13 * Finds the longest increasing path in a matrix.
14 * Each cell can move to adjacent cells (up, down, left, right) only if the value is strictly increasing.
15 *
16 * @param matrix The input matrix
17 * @return The length of the longest increasing path
18 */
19 public int longestIncreasingPath(int[][] matrix) {
20 // Initialize dimensions
21 rows = matrix.length;
22 cols = matrix[0].length;
23
24 // Initialize memoization table (0 means not computed yet)
25 memo = new int[rows][cols];
26
27 // Store reference to input matrix
28 this.matrix = matrix;
29
30 // Track the maximum path length found
31 int maxPathLength = 0;
32
33 // Try starting from each cell in the matrix
34 for (int row = 0; row < rows; row++) {
35 for (int col = 0; col < cols; col++) {
36 // Update maximum with the longest path starting from current cell
37 maxPathLength = Math.max(maxPathLength, dfs(row, col));
38 }
39 }
40
41 return maxPathLength;
42 }
43
44 /**
45 * Performs depth-first search to find the longest increasing path starting from cell (row, col).
46 * Uses memoization to avoid redundant calculations.
47 *
48 * @param row Current row position
49 * @param col Current column position
50 * @return The length of the longest increasing path starting from (row, col)
51 */
52 private int dfs(int row, int col) {
53 // Return memoized result if already computed
54 if (memo[row][col] != 0) {
55 return memo[row][col];
56 }
57
58 // Direction vectors for moving up, right, down, left
59 // dirs[i] and dirs[i+1] represent row and column offsets respectively
60 int[] directions = {-1, 0, 1, 0, -1};
61
62 // Explore all four adjacent cells
63 for (int k = 0; k < 4; k++) {
64 // Calculate coordinates of adjacent cell
65 int nextRow = row + directions[k];
66 int nextCol = col + directions[k + 1];
67
68 // Check if adjacent cell is valid and has a greater value
69 if (nextRow >= 0 && nextRow < rows &&
70 nextCol >= 0 && nextCol < cols &&
71 matrix[nextRow][nextCol] > matrix[row][col]) {
72
73 // Update the longest path from current cell
74 memo[row][col] = Math.max(memo[row][col], dfs(nextRow, nextCol));
75 }
76 }
77
78 // Add 1 to include the current cell in the path length
79 return ++memo[row][col];
80 }
81}
82
1class Solution {
2public:
3 int longestIncreasingPath(vector<vector<int>>& matrix) {
4 // Get matrix dimensions
5 int rows = matrix.size();
6 int cols = matrix[0].size();
7
8 // dp[i][j] stores the length of longest increasing path starting from (i, j)
9 // Using memoization to avoid redundant calculations
10 vector<vector<int>> dp(rows, vector<int>(cols, 0));
11
12 // Track the maximum path length found
13 int maxPathLength = 0;
14
15 // Direction vectors for exploring 4 adjacent cells (up, right, down, left)
16 int directions[5] = {-1, 0, 1, 0, -1};
17
18 // DFS function with memoization to find longest path from position (row, col)
19 function<int(int, int)> dfs = [&](int row, int col) -> int {
20 // If already computed, return the cached result
21 if (dp[row][col] != 0) {
22 return dp[row][col];
23 }
24
25 // Explore all 4 directions
26 for (int k = 0; k < 4; ++k) {
27 int nextRow = row + directions[k];
28 int nextCol = col + directions[k + 1];
29
30 // Check if next position is valid and has a greater value (increasing path)
31 if (nextRow >= 0 && nextRow < rows &&
32 nextCol >= 0 && nextCol < cols &&
33 matrix[nextRow][nextCol] > matrix[row][col]) {
34 // Update dp[row][col] with the maximum path length from adjacent cells
35 dp[row][col] = max(dp[row][col], dfs(nextRow, nextCol));
36 }
37 }
38
39 // Add 1 to include current cell in the path length
40 return ++dp[row][col];
41 };
42
43 // Try starting from each cell and find the maximum path length
44 for (int i = 0; i < rows; ++i) {
45 for (int j = 0; j < cols; ++j) {
46 maxPathLength = max(maxPathLength, dfs(i, j));
47 }
48 }
49
50 return maxPathLength;
51 }
52};
53
1/**
2 * Finds the longest increasing path in a matrix
3 * @param matrix - 2D array of numbers
4 * @returns The length of the longest increasing path
5 */
6function longestIncreasingPath(matrix: number[][]): number {
7 const rows: number = matrix.length;
8 const cols: number = matrix[0].length;
9
10 // Memoization table to store the longest path starting from each cell
11 const memo: number[][] = Array(rows)
12 .fill(0)
13 .map(() => Array(cols).fill(0));
14
15 // Direction vectors for moving up, right, down, left
16 const directions: number[] = [-1, 0, 1, 0, -1];
17
18 /**
19 * Depth-first search to find the longest increasing path from cell (row, col)
20 * @param row - Current row index
21 * @param col - Current column index
22 * @returns The length of the longest increasing path starting from this cell
23 */
24 const dfs = (row: number, col: number): number => {
25 // Return cached result if already computed
26 if (memo[row][col] > 0) {
27 return memo[row][col];
28 }
29
30 // Explore all four directions
31 for (let k = 0; k < 4; k++) {
32 const nextRow: number = row + directions[k];
33 const nextCol: number = col + directions[k + 1];
34
35 // Check if the next cell is valid and has a greater value
36 if (nextRow >= 0 && nextRow < rows &&
37 nextCol >= 0 && nextCol < cols &&
38 matrix[nextRow][nextCol] > matrix[row][col]) {
39 // Update the maximum path length for current cell
40 memo[row][col] = Math.max(memo[row][col], dfs(nextRow, nextCol));
41 }
42 }
43
44 // Add 1 to include the current cell in the path length
45 return ++memo[row][col];
46 };
47
48 let maxPathLength: number = 0;
49
50 // Try starting from each cell in the matrix
51 for (let i = 0; i < rows; i++) {
52 for (let j = 0; j < cols; j++) {
53 maxPathLength = Math.max(maxPathLength, dfs(i, j));
54 }
55 }
56
57 return maxPathLength;
58}
59
Time and Space Complexity
Time Complexity: O(m * n)
where m
is the number of rows and n
is the number of columns in the matrix.
The algorithm uses depth-first search (DFS) with memoization. Each cell (i, j)
in the matrix is visited and computed at most once due to the @cache
decorator. When a cell is first visited, the DFS explores its four neighbors and returns the longest path starting from that cell. Once computed, the result is cached and subsequent calls return immediately in O(1)
time.
Since we call dfs(i, j)
for every cell in the matrix (through the max()
comprehension), and each cell's result is computed exactly once, the total number of DFS computations is O(m * n)
. For each cell's computation, we check at most 4 neighbors, which is O(1)
work per cell. Therefore, the overall time complexity is O(m * n)
.
Space Complexity: O(m * n)
The space complexity consists of two main components:
- Cache storage: The
@cache
decorator stores the result for each unique(i, j)
pair. In the worst case, allm * n
cells will have their results cached, consumingO(m * n)
space. - Recursion call stack: In the worst case, the DFS could traverse a path that includes all cells in the matrix (imagine a strictly increasing spiral pattern). This would create a recursion depth of
O(m * n)
.
Therefore, the total space complexity is O(m * n)
.
Common Pitfalls
1. Forgetting to Memoize - Leading to Time Limit Exceeded
One of the most critical pitfalls is implementing the DFS solution without memoization. Without caching results, the algorithm repeatedly recalculates the same paths, resulting in exponential time complexity.
Problematic Code:
def longestIncreasingPath(self, matrix: List[List[int]]) -> int:
def dfs(row: int, col: int) -> int:
# NO MEMOIZATION - This will cause TLE!
max_path_length = 0
directions = [(-1, 0), (0, 1), (1, 0), (0, -1)]
for delta_row, delta_col in directions:
next_row = row + delta_row
next_col = col + delta_col
if (0 <= next_row < rows and
0 <= next_col < cols and
matrix[next_row][next_col] > matrix[row][col]):
max_path_length = max(max_path_length, dfs(next_row, next_col))
return max_path_length + 1
Solution:
Always use memoization either through @cache
decorator or manual dictionary:
from functools import cache
@cache
def dfs(row: int, col: int) -> int:
# ... rest of the logic
Or with manual memoization:
memo = {}
def dfs(row: int, col: int) -> int:
if (row, col) in memo:
return memo[(row, col)]
# ... compute result ...
memo[(row, col)] = result
return result
2. Incorrect Boundary Checking Order
Another common mistake is checking the value comparison before verifying boundaries, which can cause IndexError.
Problematic Code:
# WRONG: Checking value before boundaries if (matrix[next_row][next_col] > matrix[row][col] and # IndexError! 0 <= next_row < rows and 0 <= next_col < cols):
Solution: Always check boundaries first:
# CORRECT: Check boundaries first if (0 <= next_row < rows and 0 <= next_col < cols and matrix[next_row][next_col] > matrix[row][col]):
3. Using Wrong Comparison (Non-strict Inequality)
The problem requires a strictly increasing path, but it's easy to accidentally use >=
instead of >
.
Problematic Code:
# WRONG: Allows equal values
if matrix[next_row][next_col] >= matrix[row][col]:
max_path_length = max(max_path_length, dfs(next_row, next_col))
Solution: Use strict inequality:
# CORRECT: Strictly greater values only
if matrix[next_row][next_col] > matrix[row][col]:
max_path_length = max(max_path_length, dfs(next_row, next_col))
4. Forgetting to Add 1 for Current Cell
A subtle bug is forgetting to include the current cell in the path length count.
Problematic Code:
def dfs(row: int, col: int) -> int:
max_path_length = 0
# ... check neighbors ...
return max_path_length # WRONG: Forgot to add 1 for current cell
Solution: Always add 1 for the current cell:
def dfs(row: int, col: int) -> int:
max_path_length = 0
# ... check neighbors ...
return max_path_length + 1 # CORRECT: Include current cell
5. Improper Handling of Base Case
When a cell has no valid neighbors (all neighbors are smaller or equal), the function should return 1 (just the cell itself), not 0.
Problematic Code:
def dfs(row: int, col: int) -> int:
if no_valid_neighbors: # pseudo-code
return 0 # WRONG: Should be 1
Solution:
The correct implementation naturally handles this by returning max_path_length + 1
where max_path_length
starts at 0:
def dfs(row: int, col: int) -> int:
max_path_length = 0 # If no valid neighbors, this stays 0
# ... check neighbors ...
return max_path_length + 1 # Returns 1 when no valid neighbors
Consider the classic dynamic programming of longest increasing subsequence:
Find the length of the longest subsequence of a given sequence such that all elements of the subsequence are sorted in increasing order.
For example, the length of LIS for [50, 3, 10, 7, 40, 80]
is 4
and LIS is
[3, 7, 40, 80]
.
What is the recurrence relation?
Recommended Readings
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
https assets algo monster cover_photos graph svg Graph Fundamentals Tree with 0 cycle At this point you should be pretty familiar with trees A tree is a special kind of graph a connected acyclic cycle less graph A graph may contain cycle s and nodes could be disconnected A tree
Want a Structured Path to Master System Design Too? Donβt Miss This!