1368. Minimum Cost to Make at Least One Valid Path in a Grid
Problem Description
You are given an m x n
grid where each cell contains a directional sign (value 1-4) that tells you which cell to move to next:
1
: Move right togrid[i][j+1]
2
: Move left togrid[i][j-1]
3
: Move down togrid[i+1][j]
4
: Move up togrid[i-1][j]
Starting from the top-left cell (0, 0)
, you need to find a path that reaches the bottom-right cell (m-1, n-1)
by following the directional signs. Some signs may point outside the grid boundaries.
You can change the direction sign of any cell with a cost of 1, but each cell's sign can only be modified once. The goal is to find the minimum cost (minimum number of sign changes) needed to create at least one valid path from start to finish.
The solution uses a 0-1 BFS approach with a double-ended queue. When moving in the direction that the current cell already points to (matching the grid value), no cost is incurred and the new position is added to the front of the queue. When moving in a different direction (requiring a sign change), a cost of 1 is added and the new position is added to the back of the queue. This ensures that positions reachable with fewer changes are processed first, guaranteeing the minimum cost path is found when reaching the destination.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: The grid can be viewed as a graph where each cell is a node, and the directional signs create edges between cells. We need to find a path from the start node (0,0) to the end node (m-1, n-1).
Is it a tree?
- No: The graph is not a tree. It can have cycles (cells can point to each other in a loop), and cells may have multiple incoming edges.
Is the problem related to directed acyclic graphs?
- No: The graph is directed (each cell points in one direction), but it's not necessarily acyclic. Cells can form cycles.
Is the problem related to shortest paths?
- Yes: We need to find the minimum cost (minimum number of sign changes) to create a valid path from start to finish. This is essentially finding the shortest path where the "distance" is the number of modifications needed.
Is the graph Weighted?
- Yes: The graph has weighted edges. Following the existing direction (matching the grid value) has weight 0, while changing direction has weight 1. This creates a 0-1 weighted graph.
Dijkstra's Algorithm?
- While Dijkstra's could work here, the special property of having only 0 and 1 weights makes 0-1 BFS more efficient. This is a specialized form of BFS using a double-ended queue.
Conclusion: The flowchart leads us to recognize this as a shortest path problem in a weighted graph. The 0-1 weight property makes it ideal for BFS with a double-ended queue, where 0-weight edges are added to the front and 1-weight edges to the back of the queue.
Intuition
The key insight is to reframe this grid navigation problem as a graph shortest path problem. Each cell in the grid is a node, and we can move to any of its 4 neighbors (up, down, left, right). The twist is that moving in the direction the cell already points to is "free" (cost 0), while moving in any other direction requires changing the sign (cost 1).
This creates a special type of weighted graph where all edge weights are either 0 or 1. When we move from cell (i,j)
to an adjacent cell:
- If we move in the direction
grid[i][j]
already points to → cost = 0 - If we move in any other valid direction → cost = 1 (we need to change the sign)
For finding shortest paths in 0-1 weighted graphs, we can use a more efficient variant of BFS instead of Dijkstra's algorithm. The trick is to use a double-ended queue (deque) where:
- When we traverse an edge with weight 0, we add the new node to the front of the queue
- When we traverse an edge with weight 1, we add the new node to the back of the queue
This ordering ensures that we always process nodes in non-decreasing order of their distance from the source. Nodes reachable with fewer sign changes will be processed before those requiring more changes.
Why does this work? Because when we add a 0-cost neighbor to the front, it has the same total cost as the current node, so it should be processed with the same priority. When we add a 1-cost neighbor to the back, it has a higher cost and should be processed after all nodes with the current cost level.
By the time we reach the destination cell (m-1, n-1)
, we're guaranteed to have found the minimum number of sign changes needed to create a valid path.
Learn more about Breadth-First Search, Graph, Shortest Path and Heap (Priority Queue) patterns.
Solution Approach
The implementation uses a 0-1 BFS approach with a double-ended queue to find the minimum cost path. Let's walk through the key components:
1. Direction Mapping
dirs = [[0, 0], [0, 1], [0, -1], [1, 0], [-1, 0]]
The dirs
array maps the grid values (1-4) to their corresponding movement directions. Index 0 is unused (placeholder), while indices 1-4 represent right, left, down, and up movements respectively.
2. Initialization
q = deque([(0, 0, 0)]) # (row, col, cost)
vis = set()
We start with the source cell (0, 0)
with cost 0 in our deque. The vis
set tracks visited cells to avoid reprocessing.
3. BFS with Double-ended Queue The main loop processes cells in order of their cost:
while q: i, j, d = q.popleft() if (i, j) in vis: continue vis.add((i, j))
We pop from the left (front) of the queue and mark the cell as visited. This ensures we process cells with lower costs first.
4. Target Check
if i == m - 1 and j == n - 1: return d
If we reach the destination, we immediately return the cost, as it's guaranteed to be minimal due to our processing order.
5. Exploring Neighbors For each cell, we try all 4 possible directions:
for k in range(1, 5):
x, y = i + dirs[k][0], j + dirs[k][1]
if 0 <= x < m and 0 <= y < n:
if grid[i][j] == k:
q.appendleft((x, y, d))
else:
q.append((x, y, d + 1))
The critical logic here:
- If direction
k
matchesgrid[i][j]
(the cell's current direction), we follow it with no additional cost → add to front withappendleft((x, y, d))
- Otherwise, we need to change the sign (cost = 1) → add to back with
append((x, y, d + 1))
This ordering maintains the invariant that cells are processed in non-decreasing order of cost, ensuring optimality when we first reach the destination.
The algorithm effectively explores all reachable cells, prioritizing paths that require fewer sign changes, until it finds the minimum-cost path to the destination.
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 3x3 grid example to illustrate the 0-1 BFS solution approach:
Grid: [1, 3, 3] [3, 2, 1] [3, 1, 1] Where: 1=right, 2=left, 3=down, 4=up
Goal: Find minimum sign changes to create a path from (0,0) to (2,2).
Step-by-step execution:
Initial State:
- Deque:
[(0,0,0)]
- start at top-left with cost 0 - Visited:
{}
Iteration 1: Process (0,0) with cost 0
- Mark (0,0) as visited
- Cell value is 1 (right), so moving right to (0,1) costs 0
- Try all 4 directions from (0,0):
- Right (0,1): Matches grid value 1 → cost 0 → add to front
- Down (1,0): Doesn't match (need 3, have 1) → cost 1 → add to back
- Deque:
[(0,1,0), (1,0,1)]
Iteration 2: Process (0,1) with cost 0
- Mark (0,1) as visited
- Cell value is 3 (down), so moving down to (1,1) costs 0
- Try all 4 directions:
- Right (0,2): Doesn't match → cost 1 → add to back
- Down (1,1): Matches grid value 3 → cost 0 → add to front
- Left (0,0): Already visited, skip
- Deque:
[(1,1,0), (1,0,1), (0,2,1)]
Iteration 3: Process (1,1) with cost 0
- Mark (1,1) as visited
- Cell value is 2 (left), so moving left to (1,0) costs 0
- Try all 4 directions:
- Right (1,2): Doesn't match → cost 1 → add to back
- Down (2,1): Doesn't match → cost 1 → add to back
- Left (1,0): Matches grid value 2 → cost 0 → add to front
- Up (0,1): Already visited, skip
- Deque:
[(1,0,0), (1,0,1), (0,2,1), (1,2,1), (2,1,1)]
Iteration 4: Process (1,0) with cost 0
- Mark (1,0) as visited
- Cell value is 3 (down), so moving down to (2,0) costs 0
- Try all 4 directions:
- Right (1,1): Already visited, skip
- Down (2,0): Matches grid value 3 → cost 0 → add to front
- Up (0,0): Already visited, skip
- Deque:
[(2,0,0), (1,0,1), (0,2,1), (1,2,1), (2,1,1)]
Note: (1,0,1) in deque is duplicate but with higher cost, will be skipped when processed.
Iteration 5: Process (2,0) with cost 0
- Mark (2,0) as visited
- Cell value is 3 (down), but can't go down (out of bounds)
- Try all 4 directions:
- Right (2,1): Doesn't match → cost 1 → add to back
- Up (1,0): Already visited, skip
- Deque:
[(1,0,1), (0,2,1), (1,2,1), (2,1,1), (2,1,1)]
Continue processing... Eventually:
Key Iteration: Process (2,1) with cost 1
- Cell value is 1 (right), so moving right to (2,2) costs 0 relative to current
- Total cost to (2,2) = 1 + 0 = 1
- Deque gets:
[(2,2,1), ...]
Final: Process (2,2) with cost 1
- Reached destination! Return cost = 1
Result: Minimum 1 sign change needed. One optimal path would be:
- (0,0) → (0,1) [free, follows sign]
- (0,1) → (1,1) [free, follows sign]
- (1,1) → (1,2) [cost 1, change sign from 2 to 1]
- (1,2) → (2,2) [free, follows sign]
The 0-1 BFS ensures we find this minimum cost by always processing lower-cost paths first through the deque's front/back structure.
Solution Implementation
1from collections import deque
2from typing import List
3
4class Solution:
5 def minCost(self, grid: List[List[int]]) -> int:
6 # Get grid dimensions
7 rows, cols = len(grid), len(grid[0])
8
9 # Direction vectors: placeholder at index 0, then right, left, down, up
10 # Corresponds to grid values 1, 2, 3, 4
11 directions = [[0, 0], [0, 1], [0, -1], [1, 0], [-1, 0]]
12
13 # Initialize BFS queue with starting position (row, col, cost)
14 queue = deque([(0, 0, 0)])
15
16 # Track visited cells to avoid revisiting
17 visited = set()
18
19 # Process queue using 0-1 BFS
20 while queue:
21 current_row, current_col, current_cost = queue.popleft()
22
23 # Skip if already visited
24 if (current_row, current_col) in visited:
25 continue
26
27 # Mark current cell as visited
28 visited.add((current_row, current_col))
29
30 # Check if reached destination
31 if current_row == rows - 1 and current_col == cols - 1:
32 return current_cost
33
34 # Try all four possible directions
35 for direction_index in range(1, 5):
36 next_row = current_row + directions[direction_index][0]
37 next_col = current_col + directions[direction_index][1]
38
39 # Check if next position is within grid bounds
40 if 0 <= next_row < rows and 0 <= next_col < cols:
41 # If moving in the direction indicated by current cell (no cost)
42 if grid[current_row][current_col] == direction_index:
43 # Add to front of queue (priority for 0-cost moves)
44 queue.appendleft((next_row, next_col, current_cost))
45 else:
46 # Add to back of queue (cost increases by 1)
47 queue.append((next_row, next_col, current_cost + 1))
48
49 # Should not reach here for valid inputs
50 return -1
51
1class Solution {
2 public int minCost(int[][] grid) {
3 int rows = grid.length;
4 int cols = grid[0].length;
5
6 // Track visited cells to avoid reprocessing
7 boolean[][] visited = new boolean[rows][cols];
8
9 // Deque for 0-1 BFS: stores [row, col, cost]
10 Deque<int[]> deque = new ArrayDeque<>();
11 deque.offer(new int[] {0, 0, 0});
12
13 // Direction vectors: index 0 is dummy, 1=right, 2=left, 3=down, 4=up
14 // Matches the grid cell values (1-4)
15 int[][] directions = {{0, 0}, {0, 1}, {0, -1}, {1, 0}, {-1, 0}};
16
17 while (!deque.isEmpty()) {
18 int[] current = deque.poll();
19 int currentRow = current[0];
20 int currentCol = current[1];
21 int currentCost = current[2];
22
23 // Check if we reached the destination
24 if (currentRow == rows - 1 && currentCol == cols - 1) {
25 return currentCost;
26 }
27
28 // Skip if already visited
29 if (visited[currentRow][currentCol]) {
30 continue;
31 }
32
33 // Mark current cell as visited
34 visited[currentRow][currentCol] = true;
35
36 // Try all four directions
37 for (int direction = 1; direction <= 4; direction++) {
38 int nextRow = currentRow + directions[direction][0];
39 int nextCol = currentCol + directions[direction][1];
40
41 // Check if next position is within bounds
42 if (nextRow >= 0 && nextRow < rows && nextCol >= 0 && nextCol < cols) {
43 // If moving in the same direction as indicated by current cell
44 if (grid[currentRow][currentCol] == direction) {
45 // No cost increase - add to front of deque
46 deque.offerFirst(new int[] {nextRow, nextCol, currentCost});
47 } else {
48 // Cost increases by 1 - add to back of deque
49 deque.offer(new int[] {nextRow, nextCol, currentCost + 1});
50 }
51 }
52 }
53 }
54
55 // Should never reach here if valid path exists
56 return -1;
57 }
58}
59
1class Solution {
2public:
3 int minCost(vector<vector<int>>& grid) {
4 int rows = grid.size();
5 int cols = grid[0].size();
6
7 // Track visited cells to avoid reprocessing
8 vector<vector<bool>> visited(rows, vector<bool>(cols, false));
9
10 // Direction vectors: dummy at index 0, then right, left, down, up (matching grid values 1-4)
11 vector<vector<int>> directions = {{0, 0}, {0, 1}, {0, -1}, {1, 0}, {-1, 0}};
12
13 // Deque for 0-1 BFS: stores {encoded_position, cost}
14 // Position is encoded as row * cols + col
15 deque<pair<int, int>> dq;
16
17 // Start from top-left corner with cost 0
18 dq.push_back({0, 0});
19
20 while (!dq.empty()) {
21 // Get the cell with minimum cost (front of deque)
22 auto [encodedPos, cost] = dq.front();
23 dq.pop_front();
24
25 // Decode position
26 int row = encodedPos / cols;
27 int col = encodedPos % cols;
28
29 // Check if we reached the destination
30 if (row == rows - 1 && col == cols - 1) {
31 return cost;
32 }
33
34 // Skip if already visited
35 if (visited[row][col]) {
36 continue;
37 }
38
39 // Mark current cell as visited
40 visited[row][col] = true;
41
42 // Try all four directions
43 for (int dir = 1; dir <= 4; ++dir) {
44 int nextRow = row + directions[dir][0];
45 int nextCol = col + directions[dir][1];
46
47 // Check if next position is within bounds
48 if (nextRow >= 0 && nextRow < rows && nextCol >= 0 && nextCol < cols) {
49 int encodedNextPos = nextRow * cols + nextCol;
50
51 if (grid[row][col] == dir) {
52 // Moving in the arrow's direction (cost 0)
53 // Add to front for priority processing
54 dq.push_front({encodedNextPos, cost});
55 } else {
56 // Need to change direction (cost 1)
57 // Add to back for later processing
58 dq.push_back({encodedNextPos, cost + 1});
59 }
60 }
61 }
62 }
63
64 // Should never reach here if input is valid
65 return -1;
66 }
67};
68
1/**
2 * Finds the minimum cost to reach the bottom-right cell from the top-left cell
3 * in a grid where each cell has a direction value (1-4) and moving in a different
4 * direction than the cell's value incurs a cost of 1
5 * @param grid - 2D array where each cell contains a direction (1=right, 2=left, 3=down, 4=up)
6 * @returns The minimum cost to reach the bottom-right cell
7 */
8function minCost(grid: number[][]): number {
9 const rows: number = grid.length;
10 const cols: number = grid[0].length;
11
12 // Initialize cost matrix with infinity, except starting position
13 const costMatrix: number[][] = Array.from(
14 { length: rows },
15 () => new Array(cols).fill(Infinity)
16 );
17 costMatrix[0][0] = 0;
18
19 // Use deque for 0-1 BFS algorithm
20 const deque: [number, number][] = [[0, 0]];
21
22 // Direction vectors: right, left, down, up (corresponding to grid values 1-4)
23 const directions: [number, number][] = [
24 [0, 1], // right (direction 1)
25 [0, -1], // left (direction 2)
26 [1, 0], // down (direction 3)
27 [-1, 0], // up (direction 4)
28 ];
29
30 // Process cells using 0-1 BFS
31 while (deque.length > 0) {
32 const [currentRow, currentCol] = deque.shift()!;
33
34 // Try all four possible directions
35 for (let direction = 1; direction <= 4; direction++) {
36 const [deltaRow, deltaCol] = directions[direction - 1];
37 const nextRow: number = currentRow + deltaRow;
38 const nextCol: number = currentCol + deltaCol;
39
40 // Check if next position is within grid bounds
41 if (nextRow < 0 || nextRow >= rows || nextCol < 0 || nextCol >= cols) {
42 continue;
43 }
44
45 // Calculate cost: add 1 if we need to change direction, 0 otherwise
46 const isDirectionChange: number = grid[currentRow][currentCol] !== direction ? 1 : 0;
47 const newCost: number = isDirectionChange + costMatrix[currentRow][currentCol];
48
49 // Skip if this path doesn't improve the cost
50 if (newCost >= costMatrix[nextRow][nextCol]) {
51 continue;
52 }
53
54 // Update the cost for the next cell
55 costMatrix[nextRow][nextCol] = newCost;
56
57 // Add to deque: front if no cost added, back if cost added (0-1 BFS)
58 if (grid[currentRow][currentCol] === direction) {
59 deque.unshift([nextRow, nextCol]);
60 } else {
61 deque.push([nextRow, nextCol]);
62 }
63 }
64 }
65
66 // Return the minimum cost to reach bottom-right cell
67 return costMatrix[rows - 1][cols - 1];
68}
69
Time and Space Complexity
Time Complexity: O(m * n)
The algorithm uses a 0-1 BFS approach with a deque. Each cell (i, j)
in the grid is processed at most once due to the visited set vis
. For each cell, we explore at most 4 directions (constant time). Since we have m * n
cells total and each cell is visited once, the time complexity is O(m * n)
.
Space Complexity: O(m * n)
The space complexity consists of:
- The deque
q
which in the worst case can contain all cells:O(m * n)
- The visited set
vis
which can store up to all cells:O(m * n)
- The
dirs
array which is constant space:O(1)
Therefore, the overall space complexity is O(m * n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Using Regular BFS Instead of 0-1 BFS
A common mistake is attempting to solve this with regular BFS using a standard queue, which doesn't guarantee finding the minimum cost path.
Incorrect approach:
# Regular BFS - WRONG!
queue = deque([(0, 0, 0)])
while queue:
row, col, cost = queue.popleft()
# ... processing ...
for direction in range(1, 5):
# Always append to the back regardless of cost
queue.append((new_row, new_col, new_cost))
Why it fails: Regular BFS processes nodes in the order they're added, not by cost. You might reach the destination with a suboptimal cost before finding the minimum-cost path.
Solution: Use 0-1 BFS with a deque, adding 0-cost moves to the front and 1-cost moves to the back.
2. Visiting Cells Multiple Times
Another pitfall is allowing cells to be processed multiple times, leading to incorrect results or infinite loops.
Incorrect approach:
visited = set()
while queue:
row, col, cost = queue.popleft()
visited.add((row, col)) # Adding to visited AFTER popping
# This allows the same cell to be added to queue multiple times
for direction in range(1, 5):
if (new_row, new_col) not in visited: # Check happens too late
# Add to queue
Why it fails: The same cell can be added to the queue multiple times with different costs before it's marked as visited, leading to redundant processing and potentially wrong results.
Solution: Check visited status immediately after popping from the queue and skip if already visited:
while queue: row, col, cost = queue.popleft() if (row, col) in visited: continue # Skip already processed cells visited.add((row, col))
3. Incorrect Direction Mapping
Misaligning the direction array indices with grid values is a subtle but critical error.
Incorrect approach:
# Wrong indexing or order directions = [[0, 1], [0, -1], [1, 0], [-1, 0]] # Missing placeholder at index 0 # Or directions = [[0, 0], [1, 0], [-1, 0], [0, 1], [0, -1]] # Wrong order
Why it fails: Grid values 1-4 won't map to their intended directions, causing the algorithm to explore wrong paths.
Solution: Ensure the directions array has a placeholder at index 0 and correct mappings:
directions = [[0, 0], [0, 1], [0, -1], [1, 0], [-1, 0]] # Index 0: placeholder # Index 1: right (grid value 1) # Index 2: left (grid value 2) # Index 3: down (grid value 3) # Index 4: up (grid value 4)
4. Not Handling Edge Cases
Forgetting to handle grids where the destination is unreachable or the grid has only one cell.
Solution: The 0-1 BFS naturally handles these cases, but ensure your implementation:
- Returns 0 when the grid is 1x1 (start equals destination)
- The algorithm will explore all reachable cells, so unreachable destinations won't cause errors
- Consider adding a return statement at the end (though it shouldn't be reached in valid inputs)
Which of these properties could exist for a graph but not a tree?
Recommended Readings
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
https assets algo monster cover_photos graph svg Graph Fundamentals Tree with 0 cycle At this point you should be pretty familiar with trees A tree is a special kind of graph a connected acyclic cycle less graph A graph may contain cycle s and nodes could be disconnected A tree
Shortest Path Between A and B Prereq BFS on Graph problems graph_bfs Given an unweighted connected graph return the length of the shortest path between two nodes A and B in terms of the number of edges Assume there always exists a path between nodes A and B Input graph
Want a Structured Path to Master System Design Too? Don’t Miss This!