329. Longest Increasing Path In a Matrix - Explanation

Problem Link

Description

You are given a 2-D grid of integers matrix, where each integer is greater than or equal to 0.

Return the length of the longest strictly increasing path within matrix.

From each cell within the path, you can move either horizontally or vertically. You may not move diagonally.

Example 1:

Input: matrix = [[5,5,3],[2,3,6],[1,1,1]]

Output: 4

Explanation: The longest increasing path is [1, 2, 3, 6] or [1, 2, 3, 5].

Example 2:

Input: matrix = [[1,2,3],[2,1,4],[7,6,5]]

Output: 7

Explanation: The longest increasing path is [1, 2, 3, 4, 5, 6, 7].

Constraints:

  • 1 <= matrix.length, matrix[i].length <= 100


Topics

Recommended Time & Space Complexity

You should aim for a solution with O(m * n) time and O(m * n) space, where m is the number of rows and n is the number of columns in the given matrix.


Hint 1

If we move from one cell to an adjacent cell with a greater value, we won't revisit a cell, as the path is strictly increasing. A brute force approach would be to run a dfs from every cell and return the longest path found. This approach is exponential. Can you think of a way to optimize it to avoid redundant calculations?


Hint 2

We can use memoization to cache the results of recursive calls and avoid redundant computations. A hash map or a 2D array can be used to store these results.


Company Tags

Please upgrade to NeetCode Pro to view company tags.



Prerequisites

Before attempting this problem, you should be comfortable with:

  • Depth-First Search (DFS) - The solution explores paths in a grid using recursive DFS with 4-directional movement
  • Dynamic Programming with Memoization - Caching results for each cell to avoid redundant path computations
  • Topological Sort (Kahn's Algorithm) - An alternative BFS approach using indegrees to process cells in dependency order
  • Graph Concepts (DAG) - Understanding that strictly increasing values create a directed acyclic graph structure

1. Recursion

Intuition

We need the length of the longest path in the matrix where values strictly increase at every step. From any cell, we can move up, down, left, or right.

A natural way to solve this is to try starting from every cell and explore all increasing paths using DFS.
For a given cell, we keep walking to neighboring cells only if the next value is greater than the current one.

The recursive function represents:
"What is the longest increasing path starting from cell (r, c), given that the previous value was prevVal?"

If the move goes out of bounds or the next value is not strictly larger than prevVal, that path ends.

Algorithm

  1. Store the 4 possible movement directions (up, down, left, right).
  2. Define a recursive function dfs(r, c, prevVal):
    • If (r, c) is outside the grid, or matrix[r][c] <= prevVal, return 0
    • Otherwise, start with res = 1 (the current cell counts as length 1)
    • Try all 4 directions and take the maximum:
      • 1 + dfs(next_r, next_c, matrix[r][c])
  3. For every cell in the matrix:
    • Call dfs(r, c, -infinity) so the first cell is always allowed
    • Track the maximum result over all starting cells
  4. Return that maximum length
class Solution:
    def longestIncreasingPath(self, matrix: List[List[int]]) -> int:
        ROWS, COLS = len(matrix), len(matrix[0])
        directions = [[-1, 0], [1, 0], [0, -1], [0, 1]]

        def dfs(r, c, prevVal):
            if (min(r, c) < 0 or r >= ROWS or
                c >= COLS or matrix[r][c] <= prevVal
            ):
                return 0

            res = 1
            for d in directions:
                res = max(res, 1 + dfs(r + d[0], c + d[1], matrix[r][c]))
            return res

        LIP = 0
        for r in range(ROWS):
            for c in range(COLS):
                LIP = max(LIP, dfs(r, c, float('-inf')))
        return LIP

Time & Space Complexity

  • Time complexity: O(mn4mn)O(m * n * 4 ^ {m * n})
  • Space complexity: O(mn)O(m * n)

Where mm is the number of rows and nn is the number of columns in the given matrixmatrix.


2. Dynamic Programming (Top-Down)

Intuition

We want the length of the longest path in a grid where every move goes to a strictly larger value, and we can move in 4 directions (up, down, left, right).

A plain dfs tries many paths repeatedly. The key improvement is to notice this:

If we start from the same cell (r, c), the best (longest) increasing path from that cell is always the same.

So instead of recomputing it again and again, we store it in a cache (dp).

The recursive function is still dfs, but now it represents:
"What is the longest increasing path starting from cell (r, c)?"

We only move to neighbors that have a larger value than the current cell, and we memoize the result for each cell.

Algorithm

  1. Create a map dp to cache results:
    • dp[(r, c)] = length of the longest increasing path starting from (r, c)
  2. Define a DFS function dfs(r, c, prevVal):
    • If (r, c) is out of bounds, or matrix[r][c] <= prevVal, return 0
    • If (r, c) is already in dp, return dp[(r, c)] (reuse the cached answer)
  3. Otherwise, compute the answer for (r, c):
    • Start with res = 1 (path length including the current cell)
    • Try moving in all 4 directions and take the best valid extension:
      • res = max(res, 1 + dfs(nr, nc, matrix[r][c]))
  4. Store the computed value in dp[(r, c)] and return it
  5. Call dfs from every cell to ensure all dp values are filled
  6. The final answer is the maximum value stored in dp
class Solution:
    def longestIncreasingPath(self, matrix: List[List[int]]) -> int:
        ROWS, COLS = len(matrix), len(matrix[0])
        dp = {}  # (r, c) -> LIP

        def dfs(r, c, prevVal):
            if (r < 0 or r == ROWS or c < 0 or
                c == COLS or matrix[r][c] <= prevVal
            ):
                return 0
            if (r, c) in dp:
                return dp[(r, c)]

            res = 1
            res = max(res, 1 + dfs(r + 1, c, matrix[r][c]))
            res = max(res, 1 + dfs(r - 1, c, matrix[r][c]))
            res = max(res, 1 + dfs(r, c + 1, matrix[r][c]))
            res = max(res, 1 + dfs(r, c - 1, matrix[r][c]))
            dp[(r, c)] = res
            return res

        for r in range(ROWS):
            for c in range(COLS):
                dfs(r, c, -1)
        return max(dp.values())

Time & Space Complexity

  • Time complexity: O(mn)O(m * n)
  • Space complexity: O(mn)O(m * n)

Where mm is the number of rows and nn is the number of columns in the given matrixmatrix.


3. Topological Sort (Kahn's Algorithm)

Intuition

We can think of the matrix as a directed graph:

  • each cell is a node
  • we draw a directed edge from a smaller value to a larger value (only for 4-direction neighbors)

Because edges always go from smaller to larger, the graph has no cycles (values must strictly increase), so it is a DAG.

In a DAG, the longest path length can be found using topological order.
Kahn’s algorithm (BFS with indegrees) processes nodes level by level:

  • nodes with indegree 0 are the “smallest” starting points (no smaller neighbor points into them)
  • removing one layer may unlock the next larger layer

Each BFS layer corresponds to taking one step forward in an increasing path.
So, the number of layers processed is exactly the length of the longest increasing path.

Algorithm

  1. Treat each cell (r, c) as a node.
  2. Compute indegree[r][c]:
    • indegree[r][c] = number of neighboring cells with a smaller value (neighbors that point into (r, c))
    • For each cell, look at its 4 neighbors:
      • if a neighbor value is smaller, increase the cell's indegree
  3. Initialize a queue with all cells that have indegree 0:
    • these are local minima and can start an increasing path
  4. Perform BFS in layers:
    • for each layer:
      • pop all current nodes in the queue
      • for each popped cell, check its 4 neighbors
      • if a neighbor has a larger value, it is reachable from the current cell:
        • decrement that neighbor's indegree
        • if the neighbor's indegree becomes 0, push it into the queue
    • after finishing the layer, increment the answer (LIS += 1)
  5. When the queue becomes empty, all nodes have been processed.
  6. Return the number of layers processed (LIS), which equals the longest increasing path length.
class Solution:
    def longestIncreasingPath(self, matrix: List[List[int]]) -> int:
        ROWS, COLS = len(matrix), len(matrix[0])
        directions = [[-1, 0], [1, 0], [0, -1], [0, 1]]
        indegree = [[0] * COLS for _ in range(ROWS)]

        for r in range(ROWS):
            for c in range(COLS):
                for d in directions:
                    nr, nc = d[0] + r, d[1] + c
                    if (0 <= nr < ROWS and 0 <= nc < COLS and
                        matrix[nr][nc] < matrix[r][c]
                    ):
                        indegree[r][c] += 1

        q = deque()
        for r in range(ROWS):
            for c in range(COLS):
                if indegree[r][c] == 0:
                    q.append([r, c])

        LIS = 0
        while q:
            for _ in range(len(q)):
                r, c = q.popleft()
                for d in directions:
                    nr, nc = r + d[0], c + d[1]
                    if (0 <= nr < ROWS and 0 <= nc < COLS and
                        matrix[nr][nc] > matrix[r][c]
                    ):
                        indegree[nr][nc] -= 1
                        if indegree[nr][nc] == 0:
                            q.append([nr, nc])
            LIS += 1
        return LIS

Time & Space Complexity

  • Time complexity: O(mn)O(m * n)
  • Space complexity: O(mn)O(m * n)

Where mm is the number of rows and nn is the number of columns in the given matrixmatrix.


Common Pitfalls

Using a Visited Array Instead of Value Comparison

Unlike typical graph traversal problems, this problem does not require a visited array. The strictly increasing constraint naturally prevents revisiting cells since you cannot go back to a smaller value. Using a visited array unnecessarily complicates the solution and can cause bugs when the same cell should be explored from different starting points.

Forgetting to Try All Starting Cells

The longest increasing path might not start from any corner or edge cell. You must try starting the DFS from every cell in the matrix and track the global maximum. A common mistake is assuming the path starts from the minimum or maximum value cell.

Incorrect Memoization Key

When memoizing results, the cache key should be just the cell coordinates (r, c), not including the previous value. The longest path starting from a cell is independent of how you reached that cell because you can only move to strictly larger values. Including prevVal in the cache key wastes memory and misses cache hits.

Using Non-Strict Inequality

The problem requires strictly increasing values. Using >= instead of > when comparing neighboring cells allows equal values to be included in the path, which violates the problem constraints and leads to incorrect answers or infinite loops.

Not Handling Edge Cases for Small Matrices

For a 1x1 matrix, the answer is always 1. Some solutions fail to handle this case properly, especially when the logic assumes there will always be valid neighbors to explore.