Facebook Pixel

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 to grid[i][j+1]
  • 2: Move left to grid[i][j-1]
  • 3: Move down to grid[i+1][j]
  • 4: Move up to grid[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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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 matches grid[i][j] (the cell's current direction), we follow it with no additional cost → add to front with appendleft((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 Evaluator

Example 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)
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of these properties could exist for a graph but not a tree?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More