Facebook Pixel

2664. The Knight’s Tour 🔒

Problem Description

This problem asks you to simulate a knight's tour on a chess board. Given a board of dimensions m x n and a starting position (r, c), you need to find a path where the knight visits every cell exactly once.

The knight moves in an "L" shape, just like in chess. From any position (r1, c1), the knight can move to position (r2, c2) if:

  • The destination is within the board boundaries: 0 <= r2 <= m - 1 and 0 <= c2 <= n - 1
  • The move forms an L-shape: min(abs(r1 - r2), abs(c1 - c2)) = 1 and max(abs(r1 - r2), abs(c1 - c2)) = 2

This means the knight moves either:

  • 2 squares in one direction and 1 square perpendicular to it, or
  • 1 square in one direction and 2 squares perpendicular to it

Your task is to return a 2D array where each cell contains a number indicating the order in which that cell was visited. The starting position should have value 0, the first move should lead to a cell with value 1, the second move to value 2, and so on.

For example, with a 3 x 4 board starting at position (0, 0):

  • The knight starts at (0, 0) - marked as 0
  • Moves to (1, 2) - marked as 1
  • Moves to (2, 0) - marked as 2
  • And continues until all 12 cells are visited

The problem guarantees that at least one valid solution exists for the given inputs, so you don't need to handle impossible cases.

Flowchart Walkthrough

First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:

Is it a graph?

  • No: While the knight moves on a board which could be viewed as a graph, the problem is fundamentally about finding a specific path/sequence, not about graph traversal properties like shortest paths or connectivity.

Need to solve for kth smallest/largest?

  • No: The problem is not about finding the kth smallest or largest element. We need to find a complete tour visiting all cells.

Involves Linked Lists?

  • No: The problem involves a 2D array/board, not linked lists.

Does the problem have small constraints?

  • Yes: The constraints explicitly state 1 <= m, n <= 5, meaning the board size is at most 5x5 = 25 cells. This is a very small constraint that makes exhaustive search feasible.

Brute force / Backtracking?

  • Yes: Given the small constraints and the need to find a valid path that visits all cells exactly once, backtracking is the ideal approach. We need to:
    • Try different knight moves from each position
    • Mark cells as visited
    • Backtrack when we hit a dead end (can't make a valid move)
    • Continue until we find a complete tour

Conclusion: The flowchart correctly leads us to use a Backtracking approach. The small board size (max 5x5) and the requirement to find a specific valid path through all cells makes this a classic backtracking problem where we explore possibilities and undo moves when they don't lead to a solution.

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

Intuition

The knight's tour is a classic backtracking problem. Think of it like solving a maze where you can only move in L-shaped patterns, and you must visit every square exactly once.

The key insight is that we need to explore all possible paths systematically. When we place the knight on a square, we have up to 8 possible moves (the 8 L-shaped moves a knight can make). We need to try each move and see if it leads to a complete tour.

Here's the thought process:

  1. Start with the given position and mark it as visited (step 0).

  2. For each position, we try all 8 possible knight moves. A knight move means going 2 squares in one direction and 1 square perpendicular to it. These moves can be represented as coordinate changes: (-2,-1), (-2,1), (-1,-2), (-1,2), (1,-2), (1,2), (2,-1), (2,1).

  3. The recursive nature: From each valid position, we recursively try to complete the tour. If we successfully place the knight on a new square, we continue from there with step count incremented by 1.

  4. The backtracking part: If we reach a dead end (no valid moves available but haven't visited all squares), we need to "undo" our last move and try a different path. This is why we mark a cell as visited when we enter it and unmark it when we backtrack.

  5. Success condition: We know we've found a valid tour when the current step number equals m * n - 1 (since we start from step 0).

The beauty of backtracking here is that it guarantees we'll find a solution if one exists. We systematically explore all possibilities, and the small board size (max 5x5) makes this exhaustive search practical. The algorithm essentially builds the solution incrementally, abandoning partial solutions that cannot possibly lead to a complete tour.

Learn more about Backtracking patterns.

Solution Approach

The implementation uses depth-first search with backtracking to find a valid knight's tour. Let's walk through the key components:

Data Structure Setup:

  • Create a 2D array g of size m x n initialized with -1 to track the visit order
  • Set g[r][c] = 0 to mark the starting position
  • Use a boolean flag ok to indicate when we've found a complete tour

The DFS Function: The core logic is in the dfs(i, j) function that explores from position (i, j):

  1. Base Case Check:

    • If g[i][j] == m * n - 1, we've visited all cells (numbered from 0 to m*n-1)
    • Set ok = True and return to signal success
  2. Knight Move Generation:

    • The clever use of pairwise((-2, -1, 2, 1, -2, 1, 2, -1, -2)) generates the 8 possible knight moves
    • This creates pairs: (-2,-1), (-1,2), (2,1), (1,-2), (-2,1), (1,2), (2,-1), (-1,-2)
    • Each pair (a, b) represents the row and column offset for a knight move
  3. Exploring Moves: For each possible move to position (x, y):

    • Check if the move is valid: 0 <= x < m and 0 <= y < n
    • Check if the cell is unvisited: g[x][y] == -1
    • If valid, mark the cell: g[x][y] = g[i][j] + 1 (increment the step count)
    • Recursively call dfs(x, y) to continue the tour from the new position
  4. Backtracking:

    • After the recursive call, check if ok is True
    • If yes, we've found a solution, so return immediately (early termination)
    • If no, reset the cell: g[x][y] = -1 to try other paths
  5. Optimization with Early Termination:

    • Once ok becomes True, all recursive calls return immediately
    • This prevents unnecessary exploration after finding the first valid solution

The algorithm starts by calling dfs(r, c) from the initial position. The backtracking ensures we explore all possible paths until we find one that visits every cell exactly once. The final board g contains the visit order, which is then returned as the solution.

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 3×3 board starting at position (0,0).

Initial Setup:

  • Board dimensions: 3×3 (9 cells total)
  • Starting position: (0,0)
  • Initialize board with -1s, then mark starting position as 0:
[0] [-1] [-1]
[-1] [-1] [-1]
[-1] [-1] [-1]

Step 1: From (0,0), explore knight moves

Possible moves from (0,0):

  • (0+2, 0+1) = (2,1) ✓ Valid
  • (0+1, 0+2) = (1,2) ✓ Valid
  • All other knight moves go out of bounds

Try move to (2,1):

[0] [-1] [-1]
[-1] [-1] [-1]
[-1] [1] [-1]

Step 2: From (2,1), continue exploring

Possible moves from (2,1):

  • (2-2, 1-1) = (0,0) ✗ Already visited
  • (2-2, 1+1) = (0,2) ✓ Valid
  • (2-1, 1-2) = (1,-1) ✗ Out of bounds
  • (2-1, 1+2) = (1,3) ✗ Out of bounds
  • Continue checking... only (0,2) is valid

Move to (0,2):

[0] [-1] [2]
[-1] [-1] [-1]
[-1] [1] [-1]

Step 3: From (0,2), continue

Valid moves: (1,0) and (2,1) (already visited)

Move to (1,0):

[0] [-1] [2]
[3] [-1] [-1]
[-1] [1] [-1]

Step 4: From (1,0), continue

Valid moves: (2,2) and (0,2) (already visited)

Move to (2,2):

[0] [-1] [2]
[3] [-1] [-1]
[-1] [1] [4]

Step 5: From (2,2), dead end!

Only valid moves are to already visited cells. BACKTRACK!

Reset (2,2) to -1 and return to (1,0). No other moves from (1,0). BACKTRACK again to (0,2), then to (2,1), then to (0,0).

Try alternative path from (0,0):

This time, try (1,2) instead of (2,1):

[0] [-1] [-1]
[-1] [-1] [1]
[-1] [-1] [-1]

Continue this process... Eventually find a valid tour:

[0] [3] [6]
[7] [4] [1]
[2] [5] [8]

Key Observations:

  1. When we hit a dead end (can't visit all cells), we backtrack by:

    • Resetting the current cell to -1
    • Returning to the previous position
    • Trying the next available move
  2. The algorithm systematically explores all possibilities through DFS

  3. Once we successfully place step 8 (the 9th cell, since we start from 0), we've visited all cells and found a complete tour

  4. The ok flag prevents further exploration once a solution is found

This example demonstrates how backtracking allows us to explore different paths and undo moves when we reach dead ends, ultimately finding a valid knight's tour that visits every cell exactly once.

Solution Implementation

1class Solution:
2    def tourOfKnight(self, m: int, n: int, r: int, c: int) -> List[List[int]]:
3        """
4        Find a knight's tour on an m×n chessboard starting from position (r, c).
5        Returns a 2D grid where each cell contains the move number (0-indexed).
6      
7        Args:
8            m: Number of rows in the board
9            n: Number of columns in the board
10            r: Starting row position
11            c: Starting column position
12      
13        Returns:
14            2D list representing the board with move numbers
15        """
16      
17        def backtrack(row: int, col: int) -> None:
18            """
19            Recursively attempt to complete the knight's tour using backtracking.
20          
21            Args:
22                row: Current row position
23                col: Current column position
24            """
25            nonlocal tour_found
26          
27            # Check if we've visited all squares (0-indexed, so last move is m*n-1)
28            if board[row][col] == m * n - 1:
29                tour_found = True
30                return
31          
32            # Knight moves: 8 possible L-shaped moves
33            # Using pairwise to create (dx, dy) pairs from the sequence
34            knight_moves = (-2, -1, 2, 1, -2, 1, 2, -1, -2)
35          
36            for dx, dy in pairwise(knight_moves):
37                next_row = row + dx
38                next_col = col + dy
39              
40                # Check if the next position is valid and unvisited
41                if (0 <= next_row < m and 
42                    0 <= next_col < n and 
43                    board[next_row][next_col] == -1):
44                  
45                    # Mark the square with the move number
46                    board[next_row][next_col] = board[row][col] + 1
47                  
48                    # Recursively explore from the new position
49                    backtrack(next_row, next_col)
50                  
51                    # If a complete tour is found, stop backtracking
52                    if tour_found:
53                        return
54                  
55                    # Backtrack: unmark the square if no solution found
56                    board[next_row][next_col] = -1
57      
58        # Initialize the board with -1 (unvisited)
59        board = [[-1] * n for _ in range(m)]
60      
61        # Mark starting position as move 0
62        board[r][c] = 0
63      
64        # Flag to track if a complete tour is found
65        tour_found = False
66      
67        # Start the backtracking search
68        backtrack(r, c)
69      
70        return board
71
1class Solution {
2    // Grid to track knight's tour path
3    private int[][] grid;
4    // Dimensions of the board
5    private int rows;
6    private int cols;
7    // Flag to indicate if a valid tour is found
8    private boolean tourFound;
9
10    /**
11     * Finds a knight's tour starting from position (r, c) on an m x n board.
12     * Returns a 2D array where each cell contains the move number (0-indexed).
13     * If no complete tour exists, cells not visited will contain -1.
14     * 
15     * @param m Number of rows in the board
16     * @param n Number of columns in the board
17     * @param r Starting row position
18     * @param c Starting column position
19     * @return 2D array representing the knight's tour
20     */
21    public int[][] tourOfKnight(int m, int n, int r, int c) {
22        this.rows = m;
23        this.cols = n;
24        this.grid = new int[m][n];
25      
26        // Initialize all cells to -1 (unvisited)
27        for (int[] row : grid) {
28            Arrays.fill(row, -1);
29        }
30      
31        // Mark starting position as move 0
32        grid[r][c] = 0;
33      
34        // Start depth-first search from the starting position
35        dfs(r, c);
36      
37        return grid;
38    }
39
40    /**
41     * Performs depth-first search to find a valid knight's tour.
42     * Uses backtracking to explore all possible paths.
43     * 
44     * @param currentRow Current row position of the knight
45     * @param currentCol Current column position of the knight
46     */
47    private void dfs(int currentRow, int currentCol) {
48        // Check if all cells have been visited (tour complete)
49        if (grid[currentRow][currentCol] == rows * cols - 1) {
50            tourFound = true;
51            return;
52        }
53      
54        // Knight moves: 8 possible L-shaped moves
55        // Array represents pairs of (rowOffset, colOffset) for each move
56        int[] moveOffsets = {-2, -1, 2, 1, -2, 1, 2, -1, -2};
57      
58        // Try all 8 possible knight moves
59        for (int moveIndex = 0; moveIndex < 8; moveIndex++) {
60            int nextRow = currentRow + moveOffsets[moveIndex];
61            int nextCol = currentCol + moveOffsets[moveIndex + 1];
62          
63            // Check if the next position is valid and unvisited
64            if (nextRow >= 0 && nextRow < rows && 
65                nextCol >= 0 && nextCol < cols && 
66                grid[nextRow][nextCol] == -1) {
67              
68                // Mark the next cell with the move number
69                grid[nextRow][nextCol] = grid[currentRow][currentCol] + 1;
70              
71                // Recursively explore from the new position
72                dfs(nextRow, nextCol);
73              
74                // If a complete tour is found, stop exploring
75                if (tourFound) {
76                    return;
77                }
78              
79                // Backtrack: unmark the cell if this path doesn't lead to a solution
80                grid[nextRow][nextCol] = -1;
81            }
82        }
83    }
84}
85
1class Solution {
2public:
3    vector<vector<int>> tourOfKnight(int m, int n, int r, int c) {
4        // Initialize the board with -1 (unvisited cells)
5        vector<vector<int>> board(m, vector<int>(n, -1));
6      
7        // Mark starting position with move number 0
8        board[r][c] = 0;
9      
10        // Knight's 8 possible moves: (row offset, column offset)
11        // Stored as consecutive pairs in a single array
12        int knightMoves[9] = {-2, -1, 2, 1, -2, 1, 2, -1, -2};
13      
14        // Flag to indicate if a complete tour is found
15        bool tourFound = false;
16      
17        // Recursive DFS function to find knight's tour
18        function<void(int, int)> dfs = [&](int row, int col) {
19            // Check if we've visited all cells (complete tour)
20            if (board[row][col] == m * n - 1) {
21                tourFound = true;
22                return;
23            }
24          
25            // Try all 8 possible knight moves
26            for (int moveIdx = 0; moveIdx < 8; ++moveIdx) {
27                int nextRow = row + knightMoves[moveIdx];
28                int nextCol = col + knightMoves[moveIdx + 1];
29              
30                // Check if the next position is valid and unvisited
31                if (nextRow >= 0 && nextRow < m && 
32                    nextCol >= 0 && nextCol < n && 
33                    board[nextRow][nextCol] == -1) {
34                  
35                    // Mark the cell with the next move number
36                    board[nextRow][nextCol] = board[row][col] + 1;
37                  
38                    // Recursively explore from the new position
39                    dfs(nextRow, nextCol);
40                  
41                    // If tour is found, stop backtracking
42                    if (tourFound) {
43                        return;
44                    }
45                  
46                    // Backtrack: reset the cell to unvisited
47                    board[nextRow][nextCol] = -1;
48                }
49            }
50        };
51      
52        // Start the DFS from the initial position
53        dfs(r, c);
54      
55        // Return the board with the knight's tour (or partial tour if incomplete)
56        return board;
57    }
58};
59
1/**
2 * Finds a knight's tour on an m×n chessboard starting from position (r, c).
3 * A knight's tour is a sequence of moves where the knight visits every square exactly once.
4 * 
5 * @param m - Number of rows in the chessboard
6 * @param n - Number of columns in the chessboard
7 * @param r - Starting row position of the knight
8 * @param c - Starting column position of the knight
9 * @returns A 2D array where each cell contains the move number (0 to m*n-1), or -1 if not visited
10 */
11function tourOfKnight(m: number, n: number, r: number, c: number): number[][] {
12    // Initialize the chessboard with -1 (unvisited cells)
13    const board: number[][] = Array.from({ length: m }, () => Array(n).fill(-1));
14  
15    // Knight move directions: 8 possible L-shaped moves
16    // Each pair (dirs[i], dirs[i+1]) represents (deltaRow, deltaCol)
17    const knightMoves: number[] = [-2, -1, 2, 1, -2, 1, 2, -1, -2];
18  
19    // Flag to track if a complete tour has been found
20    let tourFound: boolean = false;
21  
22    /**
23     * Depth-first search to find a valid knight's tour
24     * @param row - Current row position
25     * @param col - Current column position
26     */
27    const dfs = (row: number, col: number): void => {
28        // Check if we've visited all squares (tour complete)
29        if (board[row][col] === m * n - 1) {
30            tourFound = true;
31            return;
32        }
33      
34        // Try all 8 possible knight moves
35        for (let moveIndex = 0; moveIndex < 8; moveIndex++) {
36            const nextRow: number = row + knightMoves[moveIndex];
37            const nextCol: number = col + knightMoves[moveIndex + 1];
38          
39            // Check if the next position is valid and unvisited
40            if (nextRow >= 0 && nextRow < m && 
41                nextCol >= 0 && nextCol < n && 
42                board[nextRow][nextCol] === -1) {
43              
44                // Mark the next cell with the move number
45                board[nextRow][nextCol] = board[row][col] + 1;
46              
47                // Recursively explore from the new position
48                dfs(nextRow, nextCol);
49              
50                // If tour is found, stop backtracking
51                if (tourFound) {
52                    return;
53                }
54              
55                // Backtrack: unmark the cell if no valid tour found
56                board[nextRow][nextCol] = -1;
57            }
58        }
59    };
60  
61    // Mark the starting position as move 0
62    board[r][c] = 0;
63  
64    // Start the depth-first search from the initial position
65    dfs(r, c);
66  
67    return board;
68}
69

Time and Space Complexity

Time Complexity: O(8^(m × n))

The algorithm uses backtracking with DFS to find a valid knight's tour on an m × n board. At each position, the knight can potentially move to up to 8 different positions (the 8 L-shaped moves a knight can make in chess). In the worst case, the algorithm explores all possible paths before finding a solution or determining no solution exists.

  • Each cell on the board can be visited once during a single path exploration
  • The total number of cells is m × n
  • At each step, we have at most 8 choices for the next move
  • The backtracking tree can have a depth of up to m × n - 1 (since we start from one cell)
  • This gives us a worst-case time complexity of O(8^(m × n))

Space Complexity: O(m × n)

The space complexity consists of:

  • The 2D grid g which stores the visit order: O(m × n)
  • The recursive call stack depth which can go up to m × n levels deep in the worst case: O(m × n)
  • Additional variables like ok, loop variables, and coordinates: O(1)

Therefore, the total space complexity is O(m × n).

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

Common Pitfalls

1. Inefficient Knight Move Representation

The current implementation uses pairwise((-2, -1, 2, 1, -2, 1, 2, -1, -2)) which is clever but can be confusing and error-prone. If the sequence is incorrect, it won't generate all 8 valid knight moves.

Better approach:

# More explicit and maintainable
knight_moves = [
    (-2, -1), (-2, 1), (-1, -2), (-1, 2),
    (1, -2), (1, 2), (2, -1), (2, 1)
]

for dx, dy in knight_moves:
    next_row = row + dx
    next_col = col + dy
    # ... rest of the logic

2. Stack Overflow for Large Boards

The recursive DFS approach can cause stack overflow for larger boards (e.g., 8×8 or bigger) due to deep recursion. Python's default recursion limit is around 1000, which may be insufficient.

Solutions:

  • Increase recursion limit: sys.setrecursionlimit(m * n + 100)
  • Use iterative approach with explicit stack
  • Implement Warnsdorff's heuristic to reduce search space

3. Performance Issues Without Heuristics

Pure backtracking explores moves in a fixed order, which can be extremely slow for certain board configurations. The algorithm might explore millions of dead-end paths before finding a solution.

Optimization with Warnsdorff's Rule:

def count_unvisited_neighbors(row, col):
    """Count how many unvisited squares a knight can reach from (row, col)"""
    count = 0
    for dx, dy in knight_moves:
        next_row, next_col = row + dx, col + dy
        if (0 <= next_row < m and 0 <= next_col < n and 
            board[next_row][next_col] == -1):
            count += 1
    return count

# In backtrack function, sort moves by accessibility
valid_moves = []
for dx, dy in knight_moves:
    next_row = row + dx
    next_col = col + dy
    if (0 <= next_row < m and 0 <= next_col < n and 
        board[next_row][next_col] == -1):
        # Store move with its accessibility count
        accessibility = count_unvisited_neighbors(next_row, next_col)
        valid_moves.append((accessibility, next_row, next_col))

# Sort by accessibility (visit squares with fewer onward options first)
valid_moves.sort()

for _, next_row, next_col in valid_moves:
    board[next_row][next_col] = board[row][col] + 1
    backtrack(next_row, next_col)
    # ... rest of backtracking logic

4. Missing Early Path Validation

For certain starting positions on small boards, finding a valid tour might be impossible or take excessive time. The current implementation doesn't detect obviously impossible cases early.

Quick validation check:

def is_tour_possible(m, n, r, c):
    """Quick heuristic check for obviously impossible cases"""
    # For very small boards (< 6 cells), tours are often impossible
    if m * n < 6:
        return False
  
    # For 3×3 board, no complete tour exists
    if m == 3 and n == 3:
        return False
  
    # For certain corner starts on small boards, tours may not exist
    # Add specific known impossible cases
  
    return True

5. Memory Usage for Very Large Boards

While not typically an issue for chess-sized boards, very large boards could consume significant memory storing the full grid.

Space-efficient alternative for path tracking:

# Instead of storing the full board, store only the path
path = [(r, c)]  # List of coordinates in visit order

# Convert path to board representation only at the end
def path_to_board(path, m, n):
    board = [[-1] * n for _ in range(m)]
    for step, (row, col) in enumerate(path):
        board[row][col] = step
    return board
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

What is the best way of checking if an element exists in a sorted array once in terms of time complexity? Select the best that applies.


Recommended Readings

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

Load More