2304. Minimum Path Cost in a Grid
Problem Description
You have a grid with m
rows and n
columns containing distinct integers from 0
to m * n - 1
. You need to find the minimum cost path from any cell in the first row to any cell in the last row.
Movement Rules:
- You can only move downward to the next row
- From cell
(x, y)
, you can move to any cell in the next row:(x + 1, 0)
,(x + 1, 1)
, ...,(x + 1, n - 1)
- You cannot move from cells in the last row
Cost Calculation:
- Each cell has a value (the number in the grid at that position)
- Moving from one cell to another has an additional movement cost
- The movement cost is given by a 2D array
moveCost
where:moveCost[i][j]
is the cost of moving from a cell with valuei
to columnj
of the next row
- The total path cost = sum of all cell values visited + sum of all movement costs
Example:
If you have a path that visits cells with values [a, b, c]
and the movement costs are [cost1, cost2]
, then:
- Total path cost =
a + b + c + cost1 + cost2
The goal is to find the path with the minimum total cost starting from any cell in the first row and ending at any cell in the last row.
Intuition
This is a classic dynamic programming problem because we need to find the optimal path and the problem has overlapping subproblems.
Think about it this way: to reach any cell in row i
, we must have come from some cell in row i-1
. The key insight is that the minimum cost to reach a cell (i, j)
depends only on the minimum costs to reach cells in the previous row.
Why? Because once we know the minimum cost to reach each cell in row i-1
, we can calculate the minimum cost to reach any cell in row i
by trying all possible moves from row i-1
and picking the cheapest option.
For each cell (i, j)
, we need to consider:
- Which cell in the previous row should we come from?
- We could come from any column
k
in rowi-1
- The cost would be: (minimum cost to reach
(i-1, k)
) + (movement cost from that cell to(i, j)
) + (value of cell(i, j)
)
This naturally leads to a dynamic programming solution where:
f[i][j]
= minimum cost to reach cell(i, j)
from the first row- We build up the solution row by row
- For each cell, we try all possible previous cells and take the minimum
Since we only need information from the previous row to calculate the current row, we can optimize space by using a rolling array - just keeping track of the costs for the previous row instead of the entire grid.
The final answer is the minimum value among all cells in the last row, since we can end at any cell in the last row.
Learn more about Dynamic Programming patterns.
Solution Approach
We implement the dynamic programming solution using a space-optimized approach with a rolling array.
Step 1: Initialize the DP array
- Start with
f = grid[0]
, which represents the minimum costs to reach each cell in the first row - These costs are just the cell values themselves since there's no movement cost to reach the first row
Step 2: Process each row iteratively
For each row i
from 1 to m-1
:
- Create a new array
g
of sizen
initialized with infinity to store the minimum costs for the current row - For each cell
(i, j)
in the current row:- Try all possible previous cells
(i-1, k)
wherek
ranges from 0 ton-1
- Calculate the cost:
f[k] + moveCost[grid[i-1][k]][j] + grid[i][j]
f[k]
: minimum cost to reach cell(i-1, k)
moveCost[grid[i-1][k]][j]
: cost to move from cell with valuegrid[i-1][k]
to columnj
grid[i][j]
: value of the current cell
- Update
g[j]
with the minimum cost found
- Try all possible previous cells
Step 3: Update the rolling array
- After processing all cells in row
i
, updatef = g
- This makes
f
now represent the minimum costs for the current row, ready for the next iteration
Step 4: Return the result
- After processing all rows,
f
contains the minimum costs to reach each cell in the last row - Return
min(f)
to get the overall minimum path cost
State Transition Formula:
f[i][j] = min{f[i-1][k] + moveCost[grid[i-1][k]][j] + grid[i][j]} for all k in [0, n-1]
Time Complexity: O(m × n²)
- For each of the m×n
cells, we check n
possible previous cells
Space Complexity: O(n)
- We only maintain arrays for the current and previous rows
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 walk through a small example to illustrate the solution approach.
Given:
- Grid (3×3):
[5, 3, 2] [1, 7, 8] [4, 6, 9]
- Movement costs (
moveCost[value][column]
):
moveCost[5][0]=1, moveCost[5][1]=2, moveCost[5][2]=3 moveCost[3][0]=4, moveCost[3][1]=1, moveCost[3][2]=2 moveCost[2][0]=2, moveCost[2][1]=3, moveCost[2][2]=1 moveCost[1][0]=3, moveCost[1][1]=2, moveCost[1][2]=1 moveCost[7][0]=1, moveCost[7][1]=3, moveCost[7][2]=2 moveCost[8][0]=2, moveCost[8][1]=1, moveCost[8][2]=3
Step 1: Initialize
f = [5, 3, 2]
(costs to reach first row cells are just their values)
Step 2: Process Row 1 Calculate minimum cost to reach each cell in row 1:
For cell (1,0) with value 1:
- From (0,0): cost = 5 + moveCost[5][0] + 1 = 5 + 1 + 1 = 7
- From (0,1): cost = 3 + moveCost[3][0] + 1 = 3 + 4 + 1 = 8
- From (0,2): cost = 2 + moveCost[2][0] + 1 = 2 + 2 + 1 = 5
- Minimum:
g[0] = 5
For cell (1,1) with value 7:
- From (0,0): cost = 5 + moveCost[5][1] + 7 = 5 + 2 + 7 = 14
- From (0,1): cost = 3 + moveCost[3][1] + 7 = 3 + 1 + 7 = 11
- From (0,2): cost = 2 + moveCost[2][1] + 7 = 2 + 3 + 7 = 12
- Minimum:
g[1] = 11
For cell (1,2) with value 8:
- From (0,0): cost = 5 + moveCost[5][2] + 8 = 5 + 3 + 8 = 16
- From (0,1): cost = 3 + moveCost[3][2] + 8 = 3 + 2 + 8 = 13
- From (0,2): cost = 2 + moveCost[2][2] + 8 = 2 + 1 + 8 = 11
- Minimum:
g[2] = 11
Update: f = [5, 11, 11]
Step 3: Process Row 2 Calculate minimum cost to reach each cell in row 2:
For cell (2,0) with value 4:
- From (1,0): cost = 5 + moveCost[1][0] + 4 = 5 + 3 + 4 = 12
- From (1,1): cost = 11 + moveCost[7][0] + 4 = 11 + 1 + 4 = 16
- From (1,2): cost = 11 + moveCost[8][0] + 4 = 11 + 2 + 4 = 17
- Minimum:
g[0] = 12
For cell (2,1) with value 6:
- From (1,0): cost = 5 + moveCost[1][1] + 6 = 5 + 2 + 6 = 13
- From (1,1): cost = 11 + moveCost[7][1] + 6 = 11 + 3 + 6 = 20
- From (1,2): cost = 11 + moveCost[8][1] + 6 = 11 + 1 + 6 = 18
- Minimum:
g[1] = 13
For cell (2,2) with value 9:
- From (1,0): cost = 5 + moveCost[1][2] + 9 = 5 + 1 + 9 = 15
- From (1,1): cost = 11 + moveCost[7][2] + 9 = 11 + 2 + 9 = 22
- From (1,2): cost = 11 + moveCost[8][2] + 9 = 11 + 3 + 9 = 23
- Minimum:
g[2] = 15
Final: f = [12, 13, 15]
Step 4: Return Result
The minimum cost path is min(12, 13, 15) = 12
The optimal path is: (0,2) → (1,0) → (2,0) with total cost 12.
Solution Implementation
1class Solution:
2 def minPathCost(self, grid: List[List[int]], moveCost: List[List[int]]) -> int:
3 # Get dimensions of the grid
4 rows, cols = len(grid), len(grid[0])
5
6 # Initialize DP array with the first row values
7 # dp[j] represents minimum cost to reach position j in current row
8 dp = grid[0]
9
10 # Process each row starting from the second row
11 for current_row in range(1, rows):
12 # Create new DP array for current row
13 new_dp = [float('inf')] * cols
14
15 # For each column in current row
16 for current_col in range(cols):
17 # Try all possible previous columns from previous row
18 for prev_col in range(cols):
19 # Calculate cost: previous cell cost + move cost + current cell value
20 # grid[current_row - 1][prev_col] is the value of the cell we're moving from
21 # moveCost[grid[current_row - 1][prev_col]][current_col] is the cost to move from that cell to current column
22 # grid[current_row][current_col] is the value of current cell
23 cost = dp[prev_col] + moveCost[grid[current_row - 1][prev_col]][current_col] + grid[current_row][current_col]
24 new_dp[current_col] = min(new_dp[current_col], cost)
25
26 # Update DP array for next iteration
27 dp = new_dp
28
29 # Return minimum cost among all ending positions in the last row
30 return min(dp)
31
1class Solution {
2 public int minPathCost(int[][] grid, int[][] moveCost) {
3 int numRows = grid.length;
4 int numCols = grid[0].length;
5
6 // Initialize dp array with the first row values
7 // dp[j] represents the minimum cost to reach column j in the current row
8 int[] dp = grid[0];
9
10 // Define a large value to represent infinity
11 final int INFINITY = 1 << 30;
12
13 // Process each row starting from the second row
14 for (int row = 1; row < numRows; ++row) {
15 // Create a new dp array for the current row
16 int[] currentRowDp = new int[numCols];
17 Arrays.fill(currentRowDp, INFINITY);
18
19 // Calculate minimum cost for each column in the current row
20 for (int col = 0; col < numCols; ++col) {
21 // Try all possible previous columns from the previous row
22 for (int prevCol = 0; prevCol < numCols; ++prevCol) {
23 // Calculate the cost: previous cell cost + move cost + current cell value
24 // grid[row - 1][prevCol] is the value of the cell we're moving from
25 // moveCost[grid[row - 1][prevCol]][col] is the cost to move from that cell to current column
26 // grid[row][col] is the value of the current cell
27 int totalCost = dp[prevCol] + moveCost[grid[row - 1][prevCol]][col] + grid[row][col];
28 currentRowDp[col] = Math.min(currentRowDp[col], totalCost);
29 }
30 }
31
32 // Update dp array for the next iteration
33 dp = currentRowDp;
34 }
35
36 // Find the minimum value in the last row
37 int minPathCost = INFINITY;
38 for (int cost : dp) {
39 minPathCost = Math.min(minPathCost, cost);
40 }
41
42 return minPathCost;
43 }
44}
45
1class Solution {
2public:
3 int minPathCost(vector<vector<int>>& grid, vector<vector<int>>& moveCost) {
4 int numRows = grid.size();
5 int numCols = grid[0].size();
6 const int INF = 1 << 30; // Large value representing infinity
7
8 // Initialize DP array with the first row values
9 // dp[j] represents minimum cost to reach column j in current row
10 vector<int> dp = grid[0];
11
12 // Process each row starting from the second row
13 for (int row = 1; row < numRows; ++row) {
14 // Temporary array to store minimum costs for current row
15 vector<int> currentRowDP(numCols, INF);
16
17 // Calculate minimum cost for each cell in current row
18 for (int currentCol = 0; currentCol < numCols; ++currentCol) {
19 // Try all possible previous columns from the previous row
20 for (int prevCol = 0; prevCol < numCols; ++prevCol) {
21 // Calculate cost: previous cell cost + move cost + current cell value
22 int totalCost = dp[prevCol] +
23 moveCost[grid[row - 1][prevCol]][currentCol] +
24 grid[row][currentCol];
25
26 // Update minimum cost for current position
27 currentRowDP[currentCol] = min(currentRowDP[currentCol], totalCost);
28 }
29 }
30
31 // Move current row results to dp array for next iteration
32 dp = move(currentRowDP);
33 }
34
35 // Return the minimum cost among all columns in the last row
36 return *min_element(dp.begin(), dp.end());
37 }
38};
39
1/**
2 * Finds the minimum path cost from the first row to the last row of a grid
3 * @param grid - 2D array representing the grid with values at each cell
4 * @param moveCost - 2D array where moveCost[i][j] represents the cost to move from value i to column j
5 * @returns The minimum total cost to reach the last row from the first row
6 */
7function minPathCost(grid: number[][], moveCost: number[][]): number {
8 const numRows: number = grid.length;
9 const numCols: number = grid[0].length;
10
11 // Initialize dp array with the first row values
12 let dpPrevRow: number[] = grid[0];
13
14 // Process each row starting from the second row
15 for (let currentRow = 1; currentRow < numRows; ++currentRow) {
16 // Initialize the dp array for the current row with infinity values
17 const dpCurrentRow: number[] = Array(numCols).fill(Infinity);
18
19 // Calculate minimum cost for each cell in the current row
20 for (let currentCol = 0; currentCol < numCols; ++currentCol) {
21 // Try all possible previous columns from the previous row
22 for (let prevCol = 0; prevCol < numCols; ++prevCol) {
23 // Calculate total cost: previous cell cost + move cost + current cell value
24 const totalCost: number = dpPrevRow[prevCol] +
25 moveCost[grid[currentRow - 1][prevCol]][currentCol] +
26 grid[currentRow][currentCol];
27
28 // Update minimum cost for current cell
29 dpCurrentRow[currentCol] = Math.min(dpCurrentRow[currentCol], totalCost);
30 }
31 }
32
33 // Update the previous row dp array with current row values
34 dpPrevRow.splice(0, numCols, ...dpCurrentRow);
35 }
36
37 // Return the minimum value from the last row
38 return Math.min(...dpPrevRow);
39}
40
Time and Space Complexity
Time Complexity: O(m × n²)
The code contains three nested loops:
- The outer loop runs
m-1
times (from row 1 to row m-1) - The middle loop runs
n
times (for each column j in the current row) - The inner loop runs
n
times (for each column k in the previous row)
For each iteration of the three loops, we perform constant time operations: accessing array elements, computing the minimum, and addition. Therefore, the total time complexity is O((m-1) × n × n) = O(m × n²)
.
Space Complexity: O(n)
The space complexity is determined by the additional space used:
- Array
f
stores the minimum costs for the previous row, usingO(n)
space - Array
g
stores the minimum costs for the current row, usingO(n)
space
Since we only maintain two arrays of size n
at any time (reusing them by reassigning f = g
), the total space complexity is O(n)
. The input arrays grid
and moveCost
are not counted in the space complexity analysis as they are given inputs.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Incorrect Movement Cost Indexing
The Problem: A frequent mistake is using the wrong indices when accessing the movement cost array. Developers often confuse:
- Using row/column indices directly instead of cell values
- Using the destination cell's value instead of the source cell's value
Incorrect Implementation:
# WRONG: Using row/column indices cost = dp[prev_col] + moveCost[current_row - 1][current_col] + grid[current_row][current_col] # WRONG: Using destination cell value cost = dp[prev_col] + moveCost[grid[current_row][current_col]][current_col] + grid[current_row][current_col]
Correct Implementation:
# CORRECT: Use the VALUE of the source cell as the first index cost = dp[prev_col] + moveCost[grid[current_row - 1][prev_col]][current_col] + grid[current_row][current_col]
Pitfall 2: Forgetting to Include Cell Values in Path Cost
The Problem: Another common error is calculating only the movement costs without including the actual cell values in the total path cost. The problem requires both the sum of cell values AND movement costs.
Incorrect Implementation:
# WRONG: Only considering movement costs cost = dp[prev_col] + moveCost[grid[current_row - 1][prev_col]][current_col]
Correct Implementation:
# CORRECT: Include both movement cost and current cell value cost = dp[prev_col] + moveCost[grid[current_row - 1][prev_col]][current_col] + grid[current_row][current_col]
Pitfall 3: Improper DP Initialization
The Problem: Failing to properly initialize the DP array for the first row. Since there's no movement cost to reach the first row, the initial costs should be just the cell values themselves.
Incorrect Implementation:
# WRONG: Initializing with zeros
dp = [0] * cols
# WRONG: Initializing with infinity
dp = [float('inf')] * cols
Correct Implementation:
# CORRECT: Initialize with first row values dp = grid[0] # or dp = grid[0].copy() if you want to be explicit
Complete Corrected Solution:
class Solution:
def minPathCost(self, grid: List[List[int]], moveCost: List[List[int]]) -> int:
rows, cols = len(grid), len(grid[0])
# Initialize with first row values (no movement cost to reach first row)
dp = grid[0].copy()
for current_row in range(1, rows):
new_dp = [float('inf')] * cols
for current_col in range(cols):
for prev_col in range(cols):
# Correct indexing: moveCost[cell_value][destination_column]
source_value = grid[current_row - 1][prev_col]
movement_cost = moveCost[source_value][current_col]
current_cell_value = grid[current_row][current_col]
# Total cost includes: previous path cost + movement cost + current cell value
total_cost = dp[prev_col] + movement_cost + current_cell_value
new_dp[current_col] = min(new_dp[current_col], total_cost)
dp = new_dp
return min(dp)
What's the output of running the following function using input [30, 20, 10, 100, 33, 12]
?
1def fun(arr: List[int]) -> List[int]:
2 import heapq
3 heapq.heapify(arr)
4 res = []
5 for i in range(3):
6 res.append(heapq.heappop(arr))
7 return res
8
1public static int[] fun(int[] arr) {
2 int[] res = new int[3];
3 PriorityQueue<Integer> heap = new PriorityQueue<>();
4 for (int i = 0; i < arr.length; i++) {
5 heap.add(arr[i]);
6 }
7 for (int i = 0; i < 3; i++) {
8 res[i] = heap.poll();
9 }
10 return res;
11}
12
1class HeapItem {
2 constructor(item, priority = item) {
3 this.item = item;
4 this.priority = priority;
5 }
6}
7
8class MinHeap {
9 constructor() {
10 this.heap = [];
11 }
12
13 push(node) {
14 // insert the new node at the end of the heap array
15 this.heap.push(node);
16 // find the correct position for the new node
17 this.bubble_up();
18 }
19
20 bubble_up() {
21 let index = this.heap.length - 1;
22
23 while (index > 0) {
24 const element = this.heap[index];
25 const parentIndex = Math.floor((index - 1) / 2);
26 const parent = this.heap[parentIndex];
27
28 if (parent.priority <= element.priority) break;
29 // if the parent is bigger than the child then swap the parent and child
30 this.heap[index] = parent;
31 this.heap[parentIndex] = element;
32 index = parentIndex;
33 }
34 }
35
36 pop() {
37 const min = this.heap[0];
38 this.heap[0] = this.heap[this.size() - 1];
39 this.heap.pop();
40 this.bubble_down();
41 return min;
42 }
43
44 bubble_down() {
45 let index = 0;
46 let min = index;
47 const n = this.heap.length;
48
49 while (index < n) {
50 const left = 2 * index + 1;
51 const right = left + 1;
52
53 if (left < n && this.heap[left].priority < this.heap[min].priority) {
54 min = left;
55 }
56 if (right < n && this.heap[right].priority < this.heap[min].priority) {
57 min = right;
58 }
59 if (min === index) break;
60 [this.heap[min], this.heap[index]] = [this.heap[index], this.heap[min]];
61 index = min;
62 }
63 }
64
65 peek() {
66 return this.heap[0];
67 }
68
69 size() {
70 return this.heap.length;
71 }
72}
73
74function fun(arr) {
75 const heap = new MinHeap();
76 for (const x of arr) {
77 heap.push(new HeapItem(x));
78 }
79 const res = [];
80 for (let i = 0; i < 3; i++) {
81 res.push(heap.pop().item);
82 }
83 return res;
84}
85
Recommended Readings
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!