Facebook Pixel

3565. Sequential Grid Path Cover πŸ”’

Problem Description

You have a 2D grid of size m x n and an integer k. The grid contains:

  • Exactly k cells with values from 1 to k (each value appears exactly once)
  • All other cells have value 0

Your task is to find a path through the grid that satisfies two conditions:

  1. Visit every cell exactly once - The path must cover all m * n cells in the grid
  2. Visit numbered cells in order - When encountering cells with values 1 through k, you must visit them in ascending order (1, then 2, then 3, and so on)

Movement rules:

  • You can start from any cell in the grid
  • From any cell, you can move to its adjacent cells (up, down, left, or right)

The output should be a 2D array of size (m * n) x 2, where each element result[i] = [xi, yi] represents the coordinates of the i-th cell visited in your path.

If multiple valid paths exist, you can return any one of them. If no valid path exists, return an empty array.

For example, if you have a 3x3 grid with k=3:

  • Cell (0,0) has value 1
  • Cell (1,1) has value 2
  • Cell (2,2) has value 3
  • All other cells have value 0

A valid path must visit all 9 cells exactly once, and when it encounters the numbered cells, it must visit cell with value 1 first, then the cell with value 2, and finally the cell with value 3.

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

Intuition

The key insight is that we need to explore all possible paths from every valid starting position to find one that satisfies both constraints. This naturally leads us to think about backtracking with DFS.

Let's think about the constraints:

  1. We must visit all cells exactly once - this is a Hamiltonian path problem
  2. We must visit numbered cells 1 to k in order

The second constraint actually helps us prune the search space. At any point in our path, we know which number we should encounter next. If we're currently looking for number v, we can only move to cells that are either 0 (empty) or have value v.

Since the grid is small (at most 6 x 6 = 36 cells), we can use bit manipulation to track visited cells efficiently. Each cell can be mapped to a unique bit position using the formula i * n + j, where i is the row and j is the column. This gives us a compact way to represent our visited state with a single integer st.

The strategy becomes:

  • Try starting from any cell that has value 0 or 1 (we need to start such that we can visit 1 first)
  • Use DFS to explore paths, keeping track of:
    • Current position (i, j)
    • Next required number v to visit in sequence
    • Visited cells using bit mask st
    • Current path being built
  • When we move to a cell with value v, increment v for the next required number
  • Only move to adjacent cells that are unvisited and have acceptable values (0 or v)
  • If we successfully build a path of length m * n, we've found our answer
  • If we get stuck, backtrack by removing the last cell from path and unmarking it as visited

The backtracking ensures we explore all possibilities, while the constraints help us avoid invalid paths early, making the search tractable despite the exponential nature of the problem.

Learn more about Recursion patterns.

Solution Approach

The solution uses state compression with DFS and backtracking to find a valid path through the grid.

State Representation: We use bit manipulation to track visited cells. Each cell (i, j) is mapped to a unique bit position using the function f(i, j) = i * n + j. The variable st is an integer where the k-th bit being 1 means the cell at position k has been visited.

Main Algorithm Structure:

  1. Helper Function f(i, j): Maps a 2D coordinate to a unique bit position in our state integer.

  2. DFS Function dfs(i, j, v):

    • Parameters:
      • (i, j): current cell position
      • v: the next numbered cell we need to visit (starts at 1)
    • Returns True if a valid complete path is found, False otherwise
  3. DFS Logic:

    • Add current cell [i, j] to the path
    • Check if we've visited all cells: len(path) == m * n. If yes, we found a solution
    • Mark current cell as visited: st |= 1 << f(i, j) (set the corresponding bit to 1)
    • If current cell value equals v, increment v for the next required number
    • Try moving in 4 directions using dirs = (-1, 0, 1, 0, -1) with pairwise iteration
    • For each adjacent cell (x, y):
      • Check if it's within bounds: 0 <= x < m and 0 <= y < n
      • Check if unvisited: (st & 1 << f(x, y)) == 0
      • Check if value is acceptable: grid[x][y] in (0, v)
      • If all conditions met, recursively call dfs(x, y, v)
    • If no valid path found from current state:
      • Backtrack by removing current cell from path: path.pop()
      • Unmark as visited: st ^= 1 << f(i, j) (flip the bit back to 0)
      • Return False
  4. Main Search Loop:

    • Iterate through all cells (i, j) in the grid
    • If grid[i][j] is 0 or 1, try starting DFS from this cell
      • We can only start from cells with value 0 or 1 because we need to visit 1 first
    • If DFS succeeds, return the path
    • If DFS fails, clear the path and reset state st = 0, then try next starting position
    • If no starting position yields a valid path, return empty array

Time Complexity: O(m * n * 2^(m*n)) in worst case, as we might explore all possible paths from each starting position.

Space Complexity: O(m * n) for the recursion stack and path storage.

The elegance of this solution lies in combining bit manipulation for efficient state tracking with constrained DFS that prunes invalid paths early by checking the numbered cell ordering requirement.

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 example with a 3x3 grid where k=2:

Grid:
[1, 0, 0]
[0, 0, 2]
[0, 0, 0]

Goal: Find a path that visits all 9 cells exactly once, visiting cell with value 1 before cell with value 2.

Step 1: Initialize

  • m = 3, n = 3, k = 2
  • path = [], st = 0 (no cells visited yet)
  • Helper function f(i,j) = i * 3 + j maps each cell to a bit position

Step 2: Try Starting Positions We can start from cells with value 0 or 1. Let's try starting from (0,0) which has value 1.

Step 3: DFS from (0,0)

  • Add [0,0] to path: path = [[0,0]]
  • Mark as visited: st = 000000001 (bit 0 set)
  • Since grid[0][0] = 1 and v = 1, increment v to 2
  • Now we need to find cell with value 2 next

Step 4: Explore from (0,0) Try moving to adjacent cells. Valid moves must be:

  • Unvisited
  • Have value 0 or 2 (since v=2)

From (0,0), we can move right to (0,1) or down to (1,0). Both have value 0, so let's try (0,1).

Step 5: DFS from (0,1)

  • Add [0,1] to path: path = [[0,0], [0,1]]
  • Mark as visited: st = 000000011 (bits 0 and 1 set)
  • grid[0,1] = 0, so v stays 2

Step 6: Continue Building Path Let's say we continue: (0,1) β†’ (0,2) β†’ (1,2) β†’ (1,1) β†’ (1,0) β†’ (2,0) β†’ (2,1) β†’ (2,2)

At (1,2), we encounter value 2:

  • When we reach (1,2), grid[1,2] = 2 and v = 2, so increment v to 3
  • Now all numbered cells have been visited in order

Step 7: Complete the Path After visiting (2,2), we have path.length = 9 = m*n, so we found a valid solution!

Final Path:

[[0,0], [0,1], [0,2], [1,2], [1,1], [1,0], [2,0], [2,1], [2,2]]

Verification:

  • βœ“ All 9 cells visited exactly once
  • βœ“ Cell with value 1 at (0,0) visited before cell with value 2 at (1,2)

If Path Fails: If at any point we get stuck (no valid moves), we backtrack:

  • Remove last cell from path using path.pop()
  • Unmark it as visited using st ^= 1 << f(i,j)
  • Try a different direction or backtrack further

The algorithm continues this process, trying different paths and starting positions until a valid path is found or all possibilities are exhausted.

Solution Implementation

1class Solution:
2    def findPath(self, grid: List[List[int]], k: int) -> List[List[int]]:
3        """
4        Find a path that visits all cells in the grid following value constraints.
5      
6        Args:
7            grid: 2D grid with integer values
8            k: Parameter (not used in this implementation)
9      
10        Returns:
11            List of [row, col] coordinates representing the path, or empty list if no path exists
12        """
13      
14        def cell_to_bitmask_index(row: int, col: int) -> int:
15            """Convert 2D grid position to a single index for bitmask representation."""
16            return row * cols + col
17      
18        def depth_first_search(row: int, col: int, current_value: int) -> bool:
19            """
20            Recursively search for a valid path using DFS.
21          
22            Args:
23                row: Current row position
24                col: Current column position
25                current_value: The next value we're allowed to step on (0 or current_value)
26          
27            Returns:
28                True if a valid path covering all cells is found, False otherwise
29            """
30            nonlocal visited_bitmask
31          
32            # Add current position to path
33            path.append([row, col])
34          
35            # Check if we've visited all cells
36            if len(path) == rows * cols:
37                return True
38          
39            # Mark current cell as visited in bitmask
40            visited_bitmask |= 1 << cell_to_bitmask_index(row, col)
41          
42            # Update the value we're looking for if current cell matches
43            if grid[row][col] == current_value:
44                current_value += 1
45          
46            # Try all four directions: up, right, down, left
47            for direction_idx in range(len(directions) - 1):
48                delta_row = directions[direction_idx]
49                delta_col = directions[direction_idx + 1]
50                next_row = row + delta_row
51                next_col = col + delta_col
52              
53                # Check if next position is valid:
54                # - Within grid bounds
55                # - Not visited yet
56                # - Has value 0 or the current expected value
57                if (0 <= next_row < rows and 
58                    0 <= next_col < cols and 
59                    (visited_bitmask & (1 << cell_to_bitmask_index(next_row, next_col))) == 0 and
60                    grid[next_row][next_col] in (0, current_value)):
61                  
62                    # Recursively explore from the next position
63                    if depth_first_search(next_row, next_col, current_value):
64                        return True
65          
66            # Backtrack: remove current position from path and mark as unvisited
67            path.pop()
68            visited_bitmask ^= 1 << cell_to_bitmask_index(row, col)
69          
70            return False
71      
72        # Initialize grid dimensions
73        rows, cols = len(grid), len(grid[0])
74      
75        # Bitmask to track visited cells (using bit manipulation for efficiency)
76        visited_bitmask = 0
77      
78        # Store the current path being explored
79        path = []
80      
81        # Direction vectors for moving up, right, down, left
82        # Stored as (delta_row, delta_col) pairs in sequence
83        directions = (-1, 0, 1, 0, -1)
84      
85        # Try starting from each cell that has value 0 or 1
86        for start_row in range(rows):
87            for start_col in range(cols):
88                if grid[start_row][start_col] in (0, 1):
89                    # Attempt to find a valid path starting from this cell
90                    if depth_first_search(start_row, start_col, 1):
91                        return path
92                  
93                    # Reset for next starting position
94                    path.clear()
95                    visited_bitmask = 0
96      
97        # No valid path found
98        return []
99
1class Solution {
2    // Grid dimensions
3    private int rows, cols;
4  
5    // Bitmask to track visited cells
6    private long visitedMask = 0;
7  
8    // Current path being explored
9    private List<List<Integer>> currentPath = new ArrayList<>();
10  
11    // Direction vectors for 4-directional movement (up, right, down, left)
12    private final int[] directions = {-1, 0, 1, 0, -1};
13
14    /**
15     * Converts 2D grid coordinates to a single index for bitmask operations
16     * @param row Row index
17     * @param col Column index
18     * @return Flattened index
19     */
20    private int flattenIndex(int row, int col) {
21        return row * cols + col;
22    }
23
24    /**
25     * Depth-first search to find a valid path through the grid
26     * @param row Current row position
27     * @param col Current column position
28     * @param targetValue Current target value to match or skip
29     * @param grid The input grid
30     * @return true if a valid path covering all cells is found
31     */
32    private boolean dfs(int row, int col, int targetValue, int[][] grid) {
33        // Add current position to path
34        currentPath.add(Arrays.asList(row, col));
35      
36        // Check if we've visited all cells
37        if (currentPath.size() == rows * cols) {
38            return true;
39        }
40      
41        // Mark current cell as visited in bitmask
42        visitedMask |= 1L << flattenIndex(row, col);
43      
44        // If current cell matches target value, increment target for next cells
45        if (grid[row][col] == targetValue) {
46            targetValue += 1;
47        }
48      
49        // Try all 4 directions
50        for (int dirIndex = 0; dirIndex < 4; dirIndex++) {
51            int deltaRow = directions[dirIndex];
52            int deltaCol = directions[dirIndex + 1];
53            int nextRow = row + deltaRow;
54            int nextCol = col + deltaCol;
55          
56            // Check if next position is valid:
57            // - Within bounds
58            // - Not visited
59            // - Cell value is 0 (wildcard) or matches target value
60            if (nextRow >= 0 && nextRow < rows && 
61                nextCol >= 0 && nextCol < cols && 
62                (visitedMask & (1L << flattenIndex(nextRow, nextCol))) == 0 &&
63                (grid[nextRow][nextCol] == 0 || grid[nextRow][nextCol] == targetValue)) {
64              
65                // Recursively explore from next position
66                if (dfs(nextRow, nextCol, targetValue, grid)) {
67                    return true;
68                }
69            }
70        }
71      
72        // Backtrack: remove current position from path and unmark as visited
73        currentPath.remove(currentPath.size() - 1);
74        visitedMask ^= 1L << flattenIndex(row, col);
75      
76        return false;
77    }
78
79    /**
80     * Finds a path through the grid that visits all cells
81     * @param grid The input grid with values
82     * @param k Parameter k (not used in current implementation)
83     * @return List of coordinates representing the path, or empty list if no path exists
84     */
85    public List<List<Integer>> findPath(int[][] grid, int k) {
86        rows = grid.length;
87        cols = grid[0].length;
88      
89        // Try starting from each cell that has value 0 or 1
90        for (int row = 0; row < rows; row++) {
91            for (int col = 0; col < cols; col++) {
92                if (grid[row][col] == 0 || grid[row][col] == 1) {
93                    // Start DFS from this cell with initial target value 1
94                    if (dfs(row, col, 1, grid)) {
95                        return currentPath;
96                    }
97                  
98                    // Reset for next attempt
99                    currentPath.clear();
100                    visitedMask = 0;
101                }
102            }
103        }
104      
105        // No valid path found
106        return List.of();
107    }
108}
109
1class Solution {
2private:
3    int rows, cols;
4    unsigned long long visited = 0;  // Bitmask to track visited cells
5    vector<vector<int>> currentPath;
6    int directions[5] = {-1, 0, 1, 0, -1};  // Direction vectors for up, right, down, left
7  
8    // Convert 2D coordinates to 1D index for bitmask
9    int getCellIndex(int row, int col) {
10        return row * cols + col;
11    }
12  
13    // DFS to find a valid path that visits all cells
14    bool dfs(int row, int col, int targetValue, vector<vector<int>>& grid) {
15        // Add current cell to path
16        currentPath.push_back({row, col});
17      
18        // Check if we've visited all cells
19        if (currentPath.size() == static_cast<size_t>(rows * cols)) {
20            return true;
21        }
22      
23        // Mark current cell as visited in bitmask
24        visited |= 1ULL << getCellIndex(row, col);
25      
26        // If current cell matches target value, increment target for next cell
27        if (grid[row][col] == targetValue) {
28            targetValue += 1;
29        }
30      
31        // Try all four directions
32        for (int dir = 0; dir < 4; ++dir) {
33            int deltaRow = directions[dir];
34            int deltaCol = directions[dir + 1];
35            int nextRow = row + deltaRow;
36            int nextCol = col + deltaCol;
37          
38            // Check if next cell is valid:
39            // 1. Within bounds
40            // 2. Not visited
41            // 3. Either empty (0) or matches the target value
42            if (nextRow >= 0 && nextRow < rows && 
43                nextCol >= 0 && nextCol < cols && 
44                (visited & (1ULL << getCellIndex(nextRow, nextCol))) == 0 &&
45                (grid[nextRow][nextCol] == 0 || grid[nextRow][nextCol] == targetValue)) {
46              
47                if (dfs(nextRow, nextCol, targetValue, grid)) {
48                    return true;
49                }
50            }
51        }
52      
53        // Backtrack: remove current cell from path and mark as unvisited
54        currentPath.pop_back();
55        visited ^= 1ULL << getCellIndex(row, col);
56      
57        return false;
58    }
59  
60public:
61    vector<vector<int>> findPath(vector<vector<int>>& grid, int k) {
62        rows = grid.size();
63        cols = grid[0].size();
64      
65        // Try starting from each cell that is either empty (0) or has value 1
66        for (int i = 0; i < rows; ++i) {
67            for (int j = 0; j < cols; ++j) {
68                if (grid[i][j] == 0 || grid[i][j] == 1) {
69                    // Attempt to find a valid path starting from this cell
70                    if (dfs(i, j, 1, grid)) {
71                        return currentPath;
72                    }
73                    // Reset for next attempt
74                    currentPath.clear();
75                    visited = 0;
76                }
77            }
78        }
79      
80        // No valid path found
81        return {};
82    }
83};
84
1// Find a path that visits all cells in the grid following specific value constraints
2function findPath(grid: number[][], k: number): number[][] {
3    const rows = grid.length;
4    const cols = grid[0].length;
5
6    // Direction vectors for moving up, right, down, left
7    const directions = [-1, 0, 1, 0, -1];
8  
9    // Store the current path being explored
10    const currentPath: number[][] = [];
11  
12    // Bitmask to track visited cells
13    let visitedMask = 0;
14
15    // Convert 2D coordinates to a single index for bitmask operations
16    function coordinateToIndex(row: number, col: number): number {
17        return row * cols + col;
18    }
19
20    // Depth-first search to find a valid path
21    function dfs(row: number, col: number, targetValue: number): boolean {
22        // Add current cell to path
23        currentPath.push([row, col]);
24      
25        // Check if we've visited all cells
26        if (currentPath.length === rows * cols) {
27            return true;
28        }
29
30        // Mark current cell as visited in bitmask
31        visitedMask |= 1 << coordinateToIndex(row, col);
32      
33        // If current cell value matches target, increment target for next cells
34        if (grid[row][col] === targetValue) {
35            targetValue += 1;
36        }
37
38        // Try all four directions
39        for (let dir = 0; dir < 4; dir++) {
40            const nextRow = row + directions[dir];
41            const nextCol = col + directions[dir + 1];
42            const nextIndex = coordinateToIndex(nextRow, nextCol);
43          
44            // Check if next cell is valid and unvisited
45            if (
46                nextRow >= 0 &&
47                nextRow < rows &&
48                nextCol >= 0 &&
49                nextCol < cols &&
50                (visitedMask & (1 << nextIndex)) === 0 &&
51                (grid[nextRow][nextCol] === 0 || grid[nextRow][nextCol] === targetValue)
52            ) {
53                // Recursively explore from next cell
54                if (dfs(nextRow, nextCol, targetValue)) {
55                    return true;
56                }
57            }
58        }
59
60        // Backtrack: remove current cell from path and mark as unvisited
61        currentPath.pop();
62        visitedMask ^= 1 << coordinateToIndex(row, col);
63      
64        return false;
65    }
66
67    // Try starting from each cell that has value 0 or 1
68    for (let row = 0; row < rows; row++) {
69        for (let col = 0; col < cols; col++) {
70            if (grid[row][col] === 0 || grid[row][col] === 1) {
71                // Reset state for new attempt
72                visitedMask = 0;
73                currentPath.length = 0;
74              
75                // Start DFS from current cell with initial target value 1
76                if (dfs(row, col, 1)) {
77                    return currentPath;
78                }
79            }
80        }
81    }
82
83    // No valid path found
84    return [];
85}
86

Time and Space Complexity

Time Complexity: O(m^2 Γ— n^2)

The analysis breaks down as follows:

  • The outer loops iterate through all m Γ— n cells in the grid to find valid starting positions, contributing O(m Γ— n) iterations.
  • For each starting position, the DFS explores paths through the grid. In the worst case, the DFS might need to explore all possible paths before finding a valid one or determining none exists.
  • At each cell during DFS, we check up to 4 neighboring cells (from the dirs array used with pairwise).
  • The maximum depth of recursion is m Γ— n (visiting all cells), and at each level, we might need to backtrack and try different paths.
  • In the worst-case scenario, for each starting position, the DFS could potentially explore a significant portion of all possible paths through the grid, which can be approximated as O(m Γ— n) operations per starting position.
  • Therefore, the total time complexity is O(m Γ— n) Γ— O(m Γ— n) = O(m^2 Γ— n^2).

Space Complexity: O(m Γ— n)

The space usage consists of:

  • The path list stores at most m Γ— n coordinate pairs (when a complete path is found).
  • The recursion stack depth can go up to m Γ— n in the worst case (visiting all cells).
  • The st variable uses bit manipulation to track visited cells, requiring O(1) space (as it's a single integer, though it represents m Γ— n bits).
  • Other variables (dirs, loop variables) use O(1) space.
  • The dominant factor is the recursion stack and the path storage, both of which are O(m Γ— n).

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Incorrect State Management During Backtracking

One of the most common pitfalls in this solution is forgetting to properly restore the state when backtracking. The code correctly handles this, but many implementations fail in one of these areas:

Pitfall Example:

# WRONG: Forgetting to restore the visited state
def dfs(row, col, current_value):
    visited_bitmask |= 1 << cell_to_bitmask_index(row, col)
    path.append([row, col])
  
    # ... recursive calls ...
  
    path.pop()  # Only removing from path
    # Missing: visited_bitmask ^= 1 << cell_to_bitmask_index(row, col)

Solution: Always ensure both the path AND the visited state are restored:

# Backtrack: remove current position from path and mark as unvisited
path.pop()
visited_bitmask ^= 1 << cell_to_bitmask_index(row, col)

2. Starting Position Logic Error

Pitfall: Starting the DFS from any cell without considering the constraint that numbered cells must be visited in order:

# WRONG: Starting from any cell
for start_row in range(rows):
    for start_col in range(cols):
        if depth_first_search(start_row, start_col, 1):
            return path

This would fail if you start from a cell with value 2 or higher, as you'd never be able to visit cell 1 first.

Solution: Only start from cells that allow visiting numbered cells in order:

# CORRECT: Only start from cells with value 0 or 1
if grid[start_row][start_col] in (0, 1):
    if depth_first_search(start_row, start_col, 1):
        return path

3. Bit Manipulation Errors

Pitfall: Using incorrect bit operations for marking/unmarking visited cells:

# WRONG: Using OR to unmark (doesn't work)
visited_bitmask |= 1 << cell_to_bitmask_index(row, col)  # Mark as visited
visited_bitmask |= 1 << cell_to_bitmask_index(row, col)  # Still marked!

# WRONG: Using AND to unmark
visited_bitmask &= 1 << cell_to_bitmask_index(row, col)  # This clears everything except the bit

Solution: Use XOR (^) to toggle bits or AND with complement to clear:

# CORRECT: Mark as visited
visited_bitmask |= 1 << cell_to_bitmask_index(row, col)

# CORRECT: Unmark as visited (toggle back)
visited_bitmask ^= 1 << cell_to_bitmask_index(row, col)

# Alternative: AND with complement
visited_bitmask &= ~(1 << cell_to_bitmask_index(row, col))

4. Value Checking Logic

Pitfall: Not properly checking if a cell can be visited based on its value:

# WRONG: Only checking if the cell equals current_value
if grid[next_row][next_col] == current_value:
    # This prevents visiting cells with value 0

Solution: Allow visiting cells with value 0 (empty cells) OR the current expected numbered value:

# CORRECT: Can visit empty cells (0) or the next required number
if grid[next_row][next_col] in (0, current_value):
    # Proceed with DFS

5. Global State Not Reset Between Starting Positions

Pitfall: Forgetting to reset the global state when trying different starting positions:

# WRONG: Not resetting state between attempts
for start_row in range(rows):
    for start_col in range(cols):
        if grid[start_row][start_col] in (0, 1):
            if depth_first_search(start_row, start_col, 1):
                return path
            # Missing reset here!

Solution: Always clear the path and reset the visited bitmask:

# CORRECT: Reset state for next attempt
if depth_first_search(start_row, start_col, 1):
    return path

# Reset for next starting position
path.clear()
visited_bitmask = 0
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Consider the classic dynamic programming of longest increasing subsequence:

Find the length of the longest subsequence of a given sequence such that all elements of the subsequence are sorted in increasing order.

For example, the length of LIS for [50, 3, 10, 7, 40, 80] is 4 and LIS is [3, 7, 40, 80].

What is the recurrence relation?


Recommended Readings

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

Load More