You are given a m x ngrid filled with non-negative numbers, find a path from top left to bottom right, which minimizes the sum of all numbers along its path.
Note: You can only move either down or right at any point in time.
Example 1:
Input: grid =[[1,2,0],[5,4,2],[1,1,3]]Output:8
Explanation: The path with minimum sum is 1 -> 2 -> 0 -> 2 -> 3.
Please upgrade to NeetCode Pro to view company tags.
Prerequisites
Before attempting this problem, you should be comfortable with:
Recursion - The brute force solution explores all paths using recursive calls
Dynamic Programming - Both top-down (memoization) and bottom-up (tabulation) approaches are used
2D Grid Traversal - Understanding how to navigate a grid moving only right or down
Space Optimization - The optimal solution reduces 2D DP to 1D by reusing a single array
1. Recursion
Intuition
From any cell, we can only move right or down. To find the minimum path sum to the bottom-right corner, we consider both choices and take the minimum. At the destination cell, we simply return its value. This naturally leads to a recursive solution where we explore all possible paths by branching at each cell.
Algorithm
Define a recursive function dfs(r, c) that returns the minimum path sum from cell (r, c) to the bottom-right.
Base case: if we reach the bottom-right cell, return grid[r][c].
If we go out of bounds (past the last row or column), return infinity to indicate an invalid path.
For each cell, return grid[r][c] + min(dfs(r+1, c), dfs(r, c+1)).
classSolution:defminPathSum(self, grid: List[List[int]])->int:defdfs(r, c):if r ==len(grid)-1and c ==len(grid[0])-1:return grid[r][c]if r ==len(grid)or c ==len(grid[0]):returnfloat('inf')return grid[r][c]+min(dfs(r +1, c), dfs(r, c +1))return dfs(0,0)
publicclassSolution{publicintminPathSum(int[][] grid){returndfs(0,0, grid);}publicintdfs(int r,int c,int[][] grid){if(r == grid.length -1&& c == grid[0].length -1){return grid[r][c];}if(r == grid.length || c == grid[0].length){returnInteger.MAX_VALUE;}return grid[r][c]+Math.min(dfs(r +1, c, grid),dfs(r, c +1, grid));}}
classSolution{public:intminPathSum(vector<vector<int>>& grid){returndfs(0,0, grid);}intdfs(int r,int c, vector<vector<int>>& grid){if(r == grid.size()-1&& c == grid[0].size()-1){return grid[r][c];}if(r == grid.size()|| c == grid[0].size()){return INT_MAX;}return grid[r][c]+min(dfs(r +1, c, grid),dfs(r, c +1, grid));}};
classSolution{/**
* @param {number[][]} grid
* @return {number}
*/minPathSum(grid){constdfs=(r, c)=>{if(r === grid.length -1&& c === grid[0].length -1){return grid[r][c];}if(r === grid.length || c === grid[0].length){returnInfinity;}return grid[r][c]+ Math.min(dfs(r +1, c),dfs(r, c +1));};returndfs(0,0);}}
publicclassSolution{publicintMinPathSum(int[][] grid){returnDfs(0,0, grid);}publicintDfs(int r,int c,int[][] grid){int M = grid.Length, N = grid[0].Length;if(r == M -1&& c == N -1){return grid[r][c];}if(r == M || c == N){returnint.MaxValue;}return grid[r][c]+ Math.Min(Dfs(r +1, c, grid),Dfs(r, c +1, grid));}}
funcminPathSum(grid [][]int)int{var dfs func(r, c int)int
dfs =func(r, c int)int{if r ==len(grid)-1&& c ==len(grid[0])-1{return grid[r][c]}if r ==len(grid)|| c ==len(grid[0]){return1<<30}return grid[r][c]+min(dfs(r+1, c),dfs(r, c+1))}returndfs(0,0)}funcmin(a, b int)int{if a < b {return a
}return b
}
class Solution {funminPathSum(grid: Array<IntArray>): Int {fundfs(r: Int, c: Int): Int {if(r == grid.size -1&& c == grid[0].size -1){return grid[r][c]}if(r == grid.size || c == grid[0].size){return Int.MAX_VALUE
}return grid[r][c]+minOf(dfs(r +1, c),dfs(r, c +1))}returndfs(0,0)}}
classSolution{funcminPathSum(_ grid:[[Int]])->Int{funcdfs(_ r:Int,_ c:Int)->Int{if r == grid.count -1&& c == grid[0].count -1{return grid[r][c]}if r == grid.count || c == grid[0].count {returnInt.max
}return grid[r][c]+min(dfs(r +1, c),dfs(r, c +1))}returndfs(0,0)}}
Time & Space Complexity
Time complexity: O(2m+n)
Space complexity: O(m+n) for recursion stack.
Where m is the number of rows and n is the number of columns.
2. Dynamic Programming (Top-Down)
Intuition
The plain recursive solution recalculates the same cells many times. For instance, both dfs(0,1) and dfs(1,0) might call dfs(1,1). By caching (memoizing) results for each cell, we ensure each subproblem is solved only once. This transforms exponential time into polynomial time.
Algorithm
Create a 2D memoization table dp initialized to -1 (indicating uncomputed).
Define dfs(r, c) as before, but first check if dp[r][c] has been computed.
If dp[r][c] != -1, return the cached value.
Otherwise, compute dp[r][c] = grid[r][c] + min(dfs(r+1, c), dfs(r, c+1)) and return it.
classSolution:defminPathSum(self, grid: List[List[int]])->int:
m, n =len(grid),len(grid[0])
dp =[[-1]* n for _ inrange(m)]defdfs(r, c):if r == m -1and c == n -1:return grid[r][c]if r == m or c == n:returnfloat('inf')if dp[r][c]!=-1:return dp[r][c]
dp[r][c]= grid[r][c]+min(dfs(r +1, c), dfs(r, c +1))return dp[r][c]return dfs(0,0)
publicclassSolution{privateint[][] dp;publicintminPathSum(int[][] grid){int m = grid.length, n = grid[0].length;
dp =newint[m][n];for(int i =0; i < m; i++){for(int j =0; j < n; j++){
dp[i][j]=-1;}}returndfs(0,0, grid);}publicintdfs(int r,int c,int[][] grid){if(r == grid.length -1&& c == grid[0].length -1){return grid[r][c];}if(r == grid.length || c == grid[0].length){returnInteger.MAX_VALUE;}if(dp[r][c]!=-1){return dp[r][c];}
dp[r][c]= grid[r][c]+Math.min(dfs(r +1, c, grid),dfs(r, c +1, grid));return dp[r][c];}}
classSolution{private:
vector<vector<int>> dp;public:intminPathSum(vector<vector<int>>& grid){int m = grid.size(), n = grid[0].size();
dp =vector<vector<int>>(m,vector<int>(n,-1));returndfs(0,0, grid);}intdfs(int r,int c, vector<vector<int>>& grid){if(r == grid.size()-1&& c == grid[0].size()-1){return grid[r][c];}if(r == grid.size()|| c == grid[0].size()){return INT_MAX;}if(dp[r][c]!=-1){return dp[r][c];}
dp[r][c]= grid[r][c]+min(dfs(r +1, c, grid),dfs(r, c +1, grid));return dp[r][c];}};
classSolution{/**
* @param {number[][]} grid
* @return {number}
*/minPathSum(grid){const m = grid.length,
n = grid[0].length;const dp = Array.from({length: m },()=>Array(n).fill(-1));constdfs=(r, c)=>{if(r === m -1&& c === n -1){return grid[r][c];}if(r === m || c === n){returnInfinity;}if(dp[r][c]!==-1){return dp[r][c];}
dp[r][c]= grid[r][c]+ Math.min(dfs(r +1, c),dfs(r, c +1));return dp[r][c];};returndfs(0,0);}}
publicclassSolution{privateint[,] dp;publicintMinPathSum(int[][] grid){int m = grid.Length, n = grid[0].Length;
dp =newint[m, n];for(int i =0; i < m; i++){for(int j =0; j < n; j++){
dp[i, j]=-1;}}returnDfs(0,0, grid);}privateintDfs(int r,int c,int[][] grid){int m = grid.Length, n = grid[0].Length;if(r == m -1&& c == n -1){return grid[r][c];}if(r == m || c == n){returnint.MaxValue;}if(dp[r, c]!=-1){return dp[r, c];}
dp[r, c]= grid[r][c]+ Math.Min(Dfs(r +1, c, grid),Dfs(r, c +1, grid));return dp[r, c];}}
funcminPathSum(grid [][]int)int{
m, n :=len(grid),len(grid[0])
dp :=make([][]int, m)for i :=range dp {
dp[i]=make([]int, n)for j :=range dp[i]{
dp[i][j]=-1}}var dfs func(r, c int)int
dfs =func(r, c int)int{if r == m-1&& c == n-1{return grid[r][c]}if r == m || c == n {return1<<30}if dp[r][c]!=-1{return dp[r][c]}
dp[r][c]= grid[r][c]+min(dfs(r+1, c),dfs(r, c+1))return dp[r][c]}returndfs(0,0)}funcmin(a, b int)int{if a < b {return a
}return b
}
class Solution {funminPathSum(grid: Array<IntArray>): Int {val m = grid.size
val n = grid[0].size
val dp =Array(m){IntArray(n){-1}}fundfs(r: Int, c: Int): Int {if(r == m -1&& c == n -1){return grid[r][c]}if(r == m || c == n){return Int.MAX_VALUE
}if(dp[r][c]!=-1){return dp[r][c]}
dp[r][c]= grid[r][c]+minOf(dfs(r +1, c),dfs(r, c +1))return dp[r][c]}returndfs(0,0)}}
classSolution{funcminPathSum(_ grid:[[Int]])->Int{let m = grid.count, n = grid[0].count
var dp =[[Int]](repeating:[Int](repeating:-1, count: n), count: m)funcdfs(_ r:Int,_ c:Int)->Int{if r == m -1&& c == n -1{return grid[r][c]}if r == m || c == n {returnInt.max
}if dp[r][c]!=-1{return dp[r][c]}
dp[r][c]= grid[r][c]+min(dfs(r +1, c),dfs(r, c +1))return dp[r][c]}returndfs(0,0)}}
Time & Space Complexity
Time complexity: O(m∗n)
Space complexity: O(m∗n)
Where m is the number of rows and n is the number of columns.
3. Dynamic Programming (Bottom-Up)
Intuition
Instead of recursing from top-left to bottom-right, we can fill a DP table starting from the bottom-right corner. For each cell, we know the minimum path sum to reach the destination from the cell below and from the cell to the right. We take the minimum of these two and add the current cell value. This iterative approach avoids recursion overhead.
Algorithm
Create a DP table of size (ROWS+1) x (COLS+1) initialized to infinity. The extra row and column handle boundary conditions.
Set dp[ROWS-1][COLS] = 0 as the base case (one position past the destination, allowing the destination cell to be computed correctly).
Iterate from bottom-right to top-left: for each cell (r, c), set dp[r][c] = grid[r][c] + min(dp[r+1][c], dp[r][c+1]).
publicclassSolution{publicintminPathSum(int[][] grid){intROWS= grid.length,COLS= grid[0].length;int[][] dp =newint[ROWS+1][COLS+1];for(int r =0; r <=ROWS; r++){for(int c =0; c <=COLS; c++){
dp[r][c]=Integer.MAX_VALUE;}}
dp[ROWS-1][COLS]=0;for(int r =ROWS-1; r >=0; r--){for(int c =COLS-1; c >=0; c--){
dp[r][c]= grid[r][c]+Math.min(dp[r +1][c], dp[r][c +1]);}}return dp[0][0];}}
classSolution{public:intminPathSum(vector<vector<int>>& grid){int ROWS = grid.size(), COLS = grid[0].size();
vector<vector<int>>dp(ROWS +1,vector<int>(COLS +1, INT_MAX));
dp[ROWS -1][COLS]=0;for(int r = ROWS -1; r >=0; r--){for(int c = COLS -1; c >=0; c--){
dp[r][c]= grid[r][c]+min(dp[r +1][c], dp[r][c +1]);}}return dp[0][0];}};
classSolution{/**
* @param {number[][]} grid
* @return {number}
*/minPathSum(grid){constROWS= grid.length,COLS= grid[0].length;const dp = Array.from({length:ROWS+1},()=>Array(COLS+1).fill(Infinity),);
dp[ROWS-1][COLS]=0;for(let r =ROWS-1; r >=0; r--){for(let c =COLS-1; c >=0; c--){
dp[r][c]= grid[r][c]+ Math.min(dp[r +1][c], dp[r][c +1]);}}return dp[0][0];}}
publicclassSolution{publicintMinPathSum(int[][] grid){int ROWS = grid.Length, COLS = grid[0].Length;int[,] dp =newint[ROWS +1, COLS +1];for(int r =0; r <= ROWS; r++){for(int c =0; c <= COLS; c++){
dp[r, c]=int.MaxValue;}}
dp[ROWS -1, COLS]=0;for(int r = ROWS -1; r >=0; r--){for(int c = COLS -1; c >=0; c--){
dp[r, c]= grid[r][c]+ Math.Min(dp[r +1, c], dp[r, c +1]);}}return dp[0,0];}}
funcminPathSum(grid [][]int)int{
ROWS, COLS :=len(grid),len(grid[0])
dp :=make([][]int, ROWS+1)for i :=range dp {
dp[i]=make([]int, COLS+1)for j :=range dp[i]{
dp[i][j]=1<<30}}
dp[ROWS-1][COLS]=0for r := ROWS -1; r >=0; r--{for c := COLS -1; c >=0; c--{
dp[r][c]= grid[r][c]+min(dp[r+1][c], dp[r][c+1])}}return dp[0][0]}funcmin(a, b int)int{if a < b {return a
}return b
}
class Solution {funminPathSum(grid: Array<IntArray>): Int {val ROWS = grid.size
val COLS = grid[0].size
val dp =Array(ROWS +1){IntArray(COLS +1){ Int.MAX_VALUE }}
dp[ROWS -1][COLS]=0for(r in ROWS -1 downTo 0){for(c in COLS -1 downTo 0){
dp[r][c]= grid[r][c]+minOf(dp[r +1][c], dp[r][c +1])}}return dp[0][0]}}
classSolution{funcminPathSum(_ grid:[[Int]])->Int{letROWS= grid.count,COLS= grid[0].count
var dp =[[Int]](repeating:[Int](repeating:Int.max, count:COLS+1), count:ROWS+1)
dp[ROWS-1][COLS]=0for r instride(from:ROWS-1, through:0, by:-1){for c instride(from:COLS-1, through:0, by:-1){
dp[r][c]= grid[r][c]+min(dp[r +1][c], dp[r][c +1])}}return dp[0][0]}}
Time & Space Complexity
Time complexity: O(m∗n)
Space complexity: O(m∗n)
Where m is the number of rows and n is the number of columns.
4. Dynamic Programming (Space Optimized)
Intuition
When filling the DP table row by row (from bottom to top), we only need the current row and the row below. In fact, since we process columns right to left, we can overwrite the same 1D array. The value at dp[c] represents the minimum path sum from (r+1, c), and dp[c+1] represents the path from (r, c+1). After updating, dp[c] will hold the result for (r, c).
Algorithm
Create a 1D array dp of size COLS+1 initialized to infinity.
Set dp[COLS-1] = 0 as the base case.
For each row from bottom to top:
For each column from right to left: dp[c] = grid[r][c] + min(dp[c], dp[c+1]).
publicclassSolution{publicintminPathSum(int[][] grid){intROWS= grid.length,COLS= grid[0].length;int[] dp =newint[COLS+1];for(int c =0; c <=COLS; c++){
dp[c]=Integer.MAX_VALUE;}
dp[COLS-1]=0;for(int r =ROWS-1; r >=0; r--){for(int c =COLS-1; c >=0; c--){
dp[c]= grid[r][c]+Math.min(dp[c], dp[c +1]);}}return dp[0];}}
classSolution{public:intminPathSum(vector<vector<int>>& grid){int ROWS = grid.size(), COLS = grid[0].size();
vector<int>dp(COLS +1, INT_MAX);
dp[COLS -1]=0;for(int r = ROWS -1; r >=0; r--){for(int c = COLS -1; c >=0; c--){
dp[c]= grid[r][c]+min(dp[c], dp[c +1]);}}return dp[0];}};
classSolution{/**
* @param {number[][]} grid
* @return {number}
*/minPathSum(grid){constROWS= grid.length,COLS= grid[0].length;const dp =newArray(COLS+1).fill(Infinity);
dp[COLS-1]=0;for(let r =ROWS-1; r >=0; r--){for(let c =COLS-1; c >=0; c--){
dp[c]= grid[r][c]+ Math.min(dp[c], dp[c +1]);}}return dp[0];}}
publicclassSolution{publicintMinPathSum(int[][] grid){int ROWS = grid.Length, COLS = grid[0].Length;int[] dp =newint[COLS +1];for(int c =0; c <= COLS; c++){
dp[c]=int.MaxValue;}
dp[COLS -1]=0;for(int r = ROWS -1; r >=0; r--){for(int c = COLS -1; c >=0; c--){
dp[c]= grid[r][c]+ Math.Min(dp[c], dp[c +1]);}}return dp[0];}}
funcminPathSum(grid [][]int)int{
ROWS, COLS :=len(grid),len(grid[0])
dp :=make([]int, COLS+1)for i :=range dp {
dp[i]=1<<30}
dp[COLS-1]=0for r := ROWS -1; r >=0; r--{for c := COLS -1; c >=0; c--{
dp[c]= grid[r][c]+min(dp[c], dp[c+1])}}return dp[0]}funcmin(a, b int)int{if a < b {return a
}return b
}
class Solution {funminPathSum(grid: Array<IntArray>): Int {val ROWS = grid.size
val COLS = grid[0].size
val dp =IntArray(COLS +1){ Int.MAX_VALUE }
dp[COLS -1]=0for(r in ROWS -1 downTo 0){for(c in COLS -1 downTo 0){
dp[c]= grid[r][c]+minOf(dp[c], dp[c +1])}}return dp[0]}}
classSolution{funcminPathSum(_ grid:[[Int]])->Int{letROWS= grid.count,COLS= grid[0].count
var dp =[Int](repeating:Int.max, count:COLS+1)
dp[COLS-1]=0for r instride(from:ROWS-1, through:0, by:-1){for c instride(from:COLS-1, through:0, by:-1){
dp[c]= grid[r][c]+min(dp[c], dp[c +1])}}return dp[0]}}
Time & Space Complexity
Time complexity: O(m∗n)
Space complexity: O(n)
Where m is the number of rows and n is the number of columns.
Common Pitfalls
Incorrect Boundary Initialization
When using bottom-up DP, the extra row and column must be initialized to infinity (or a very large value), except for one cell that serves as the base case. Initializing boundaries to 0 instead of infinity causes the algorithm to prefer going out of bounds, producing incorrect minimum sums.
Integer Overflow with MAX_VALUE
When adding grid[r][c] to Integer.MAX_VALUE (or equivalent), the result overflows to a negative number, which then incorrectly becomes the minimum. Use a large but safe value like 1 << 30 instead, or add overflow checks before the addition.
Wrong Iteration Direction in Space-Optimized Solution
In the 1D DP optimization, the iteration direction matters. When processing from bottom-right to top-left, you must iterate columns right-to-left within each row. Iterating left-to-right overwrites values before they are used, corrupting the DP state and producing wrong answers.