The matrix is secretly a DAG — each cell points only to strictly-larger neighbors, so there are no cycles. DFS with per-cell memoization computes the longest path from every cell in O(m·n) total, visiting each cell at most twice.
Given an m × n integer matrix, return the length of the longest strictly increasing path. From any cell you may move up, down, left, or right — never diagonally — and only to a strictly larger value. You cannot revisit cells.
Worked example:[[9,9,4],[6,6,8],[2,1,1]] → 4
The path 1 → 2 → 6 → 9 (row 2 col 1 → row 2 col 0 → row 1 col 0 → row 0 col 0) has length 4.
Map (or 2D array) stores results for cells already computed. Without it, repeated DFS would cost O(m²·n²) in the worst case.(nr, nc) qualifies only if it is in bounds and matrix[nr][nc] > matrix[r][c]. Why strict? To form an increasing path — equal values would allow cycles.dp[r][c] = 1 + max(dp[nr][nc]) over valid neighbors. If no valid neighbor exists, the cell itself is a length-1 path.dfs(r, c) for every cell and track the global maximum. The answer is the largest value across all starting cells.v, every reachable cell has value > v, so you can never revisit the current cell. Adding a visited set would break memoization and produce wrong answers on paths that share sub-paths.Topological sort (Kahn's): Build the explicit DAG, compute in-degrees, BFS layer by layer. Same O(m·n) time, O(m·n) space, but more code. The memoized DFS is simpler and preferred in interviews.
1▶function longestIncreasingPath(matrix: number[][]): number {2▶ const rows = matrix.length;3▶ const cols = matrix[0].length;4▶ const memo = new Map<number, number>();56 function encode(r: number, c: number): number {7 return r * cols + c;8 }910 function dfs(r: number, c: number): number {11 const key = encode(r, c);12 if (memo.has(key)) return memo.get(key)!;1314 const dirs: [number, number][] = [[-1,0],[1,0],[0,-1],[0,1]];15 let best = 1; // cell itself counts16 for (const [dr, dc] of dirs) {17 const nr = r + dr;18 const nc = c + dc;19 if (nr >= 0 && nr < rows && nc >= 0 && nc < cols20 && matrix[nr][nc] > matrix[r][c]) { // strictly greater21 best = Math.max(best, 1 + dfs(nr, nc));22 }23 }2425 memo.set(key, best);26 return best;27 }2829▶ let ans = 0;30▶ for (let r = 0; r < rows; r++) {31▶ for (let c = 0; c < cols; c++) {32 ans = Math.max(ans, dfs(r, c));33 }34 }35 return ans;36}
function longestIncreasingPath(matrix: number[][]): number {
const rows = matrix.length;
const cols = matrix[0].length;
const memo = new Map<number, number>();
function encode(r: number, c: number): number {
return r * cols + c;
}
function dfs(r: number, c: number): number {
const key = encode(r, c);
if (memo.has(key)) return memo.get(key)!;
const dirs: [number, number][] = [[-1,0],[1,0],[0,-1],[0,1]];
let best = 1; // cell itself counts
for (const [dr, dc] of dirs) {
const nr = r + dr;
const nc = c + dc;
if (nr >= 0 && nr < rows && nc >= 0 && nc < cols
&& matrix[nr][nc] > matrix[r][c]) { // strictly greater
best = Math.max(best, 1 + dfs(nr, nc));
}
}
memo.set(key, best);
return best;
}
let ans = 0;
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
ans = Math.max(ans, dfs(r, c));
}
}
return ans;
}rows and cols so every bounds check avoids re-accessing matrix.length.Map<number, number> stores results keyed by r * cols + c — the unique integer id of each cell. Using a flat key avoids allocating a 2D array.best starts at 1 because the cell itself counts as a path of length 1.best before returning so future callers skip the DFS entirely. Without this, the same sub-paths would be recomputed repeatedly.> to >= — but then you need a real visited set per DFS call because cycles become possible.parent map alongside memo and reconstruct by following the chosen neighbor from the cell with the global maximum dp value.dirs array to 8 directions. The DAG property and memoization strategy remain unchanged.dirs to 6 directions (±x, ±y, ±z) and encode with r * cols * depth + c * depth + d.function longestIncreasingPath(matrix: number[][]): number {
const rows = matrix.length;
const cols = matrix[0].length;
const memo = new Map<number, number>();
function encode(r: number, c: number): number {
return r * cols + c;
}
function dfs(r: number, c: number): number {
const key = encode(r, c);
if (memo.has(key)) return memo.get(key)!;
const dirs: [number, number][] = [[-1,0],[1,0],[0,-1],[0,1]];
let best = 1; // cell itself counts
for (const [dr, dc] of dirs) {
const nr = r + dr;
const nc = c + dc;
if (nr >= 0 && nr < rows && nc >= 0 && nc < cols
&& matrix[nr][nc] > matrix[r][c]) { // strictly greater
best = Math.max(best, 1 + dfs(nr, nc));
}
}
memo.set(key, best);
return best;
}
let ans = 0;
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
ans = Math.max(ans, dfs(r, c));
}
}
return ans;
}Treat each cell as a DAG node pointing to strictly-greater neighbors, then peel off the "sink" cells layer by layer — the number of layers is the longest path.
function longestIncreasingPath(matrix: number[][]): number {
const rows = matrix.length;
const cols = matrix[0].length;
const dirs: [number, number][] = [[-1, 0], [1, 0], [0, -1], [0, 1]];
// outDegree[r][c] = number of strictly-greater neighbors (edges OUT of this cell).
const outDegree: number[][] = Array.from({ length: rows }, () =>
new Array<number>(cols).fill(0),
);
// Cells with out-degree 0 are local peaks — the "sinks" of the DAG.
const queue: [number, number][] = [];
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
for (const [dr, dc] of dirs) {
const nr = r + dr;
const nc = c + dc;
if (
nr >= 0 && nr < rows && nc >= 0 && nc < cols &&
matrix[nr][nc] > matrix[r][c]
) {
outDegree[r][c]++;
}
}
if (outDegree[r][c] === 0) queue.push([r, c]);
}
}
// Peel layer by layer. Each round removes the current peaks; a smaller
// neighbor whose only-larger edge pointed here loses an out-edge and may
// itself become a peak in the next round.
let layers = 0;
let frontier = queue;
while (frontier.length > 0) {
layers++;
const next: [number, number][] = [];
for (const [r, c] of frontier) {
for (const [dr, dc] of dirs) {
const nr = r + dr;
const nc = c + dc;
if (
nr >= 0 && nr < rows && nc >= 0 && nc < cols &&
matrix[nr][nc] < matrix[r][c] // neighbor pointed UP to us
) {
outDegree[nr][nc]--;
if (outDegree[nr][nc] === 0) next.push([nr, nc]);
}
}
}
frontier = next;
}
return layers;
}The same recurrence as the optimal solution but with the memo table removed — correct, yet it recomputes shared sub-paths exponentially and TLEs on large grids.
function longestIncreasingPath(matrix: number[][]): number {
const rows = matrix.length;
const cols = matrix[0].length;
const dirs: [number, number][] = [[-1, 0], [1, 0], [0, -1], [0, 1]];
// No memo: every call re-explores the entire sub-DAG below (r, c).
function dfs(r: number, c: number): number {
let best = 1; // the cell itself
for (const [dr, dc] of dirs) {
const nr = r + dr;
const nc = c + dc;
if (
nr >= 0 && nr < rows && nc >= 0 && nc < cols &&
matrix[nr][nc] > matrix[r][c] // strictly greater
) {
best = Math.max(best, 1 + dfs(nr, nc));
}
}
return best;
}
let ans = 0;
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
ans = Math.max(ans, dfs(r, c));
}
}
return ans;
}Map cache (the optimal solution) collapses this to O(m·n).| "longest path in a grid, only strictly greater" | DFS + per-cell memo (DAG DP) |
| matrix traversal, no revisit, monotone constraint | implicit DAG → no visited set needed |
| "starting from any cell" | outer loop calling dfs() on every cell, take max |
| path-length DP in a graph/grid | 1 + max(dfs(neighbor)) recurrence |
const memo = new Map<number, number>();
const encode = (r: number, c: number) => r * cols + c;
function dfs(r: number, c: number): number {
const key = encode(r, c);
if (memo.has(key)) return memo.get(key)!;
let best = 1;
for (const [dr, dc] of [[-1,0],[1,0],[0,-1],[0,1]]) {
const nr = r + dr, nc = c + dc;
if (inBounds && matrix[nr][nc] > matrix[r][c])
best = Math.max(best, 1 + dfs(nr, nc));
}
memo.set(key, best);
return best;
}
// outer loop: call dfs every cell, track max[[9,9,4],[6,6,8],[2,1,1]]. What is the longest increasing path length?