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: 4Explanation: 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: 7Explanation: The longest increasing path is [1, 2, 3, 4, 5, 6, 7].
Constraints:
1 <= matrix.length, matrix[i].length <= 100
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.
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?
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.
Before attempting this problem, you should be comfortable with:
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.
dfs(r, c, prevVal):(r, c) is outside the grid, or matrix[r][c] <= prevVal, return 0res = 1 (the current cell counts as length 1)1 + dfs(next_r, next_c, matrix[r][c])dfs(r, c, -infinity) so the first cell is always allowedclass 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 LIPWhere is the number of rows and is the number of columns in the given .
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.
dp to cache results:dp[(r, c)] = length of the longest increasing path starting from (r, c)dfs(r, c, prevVal):(r, c) is out of bounds, or matrix[r][c] <= prevVal, return 0(r, c) is already in dp, return dp[(r, c)] (reuse the cached answer)(r, c):res = 1 (path length including the current cell)res = max(res, 1 + dfs(nr, nc, matrix[r][c]))dp[(r, c)] and return itdfs from every cell to ensure all dp values are filleddpclass 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())Where is the number of rows and is the number of columns in the given .
We can think of the matrix as a directed graph:
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:
0 are the “smallest” starting points (no smaller neighbor points into them)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.
(r, c) as a node.indegree[r][c]:indegree[r][c] = number of neighboring cells with a smaller value (neighbors that point into (r, c))0:indegreeindegree becomes 0, push it into the queueLIS += 1)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 LISWhere is the number of rows and is the number of columns in the given .
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.
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.
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.
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.
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.