Facebook Pixel

688. Knight Probability in Chessboard

Problem Description

You have an n x n chessboard with a knight piece starting at position (row, column). The board uses 0-based indexing, meaning the top-left corner is (0, 0) and the bottom-right corner is (n - 1, n - 1).

A knight in chess moves in an "L" shape - it moves two squares in one direction (horizontal or vertical) and then one square perpendicular to that direction. This gives the knight 8 possible moves from any position.

The knight will make exactly k moves. For each move, it randomly chooses one of the 8 possible moves with equal probability (1/8 chance for each move), regardless of whether that move would take it off the board. If the knight moves off the board, it cannot return.

Your task is to calculate the probability that after making exactly k moves, the knight is still on the chessboard.

For example, if n = 3, k = 2, and the knight starts at (0, 0):

  • The knight has 8 possible first moves, but only 2 keep it on the board
  • From each valid position after the first move, some second moves keep it on board and others don't
  • The final probability is the sum of all paths that keep the knight on board, weighted by their probabilities

The solution uses dynamic programming where f[h][i][j] represents the probability of reaching position (i, j) after h moves while staying on the board. Initially, f[0][i][j] = 1 for all valid positions since the knight starts on the board. For each subsequent move h, the probability at each position is calculated by summing the probabilities from all valid previous positions that could reach it, each multiplied by 1/8 (the probability of choosing that particular move).

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

Intuition

The key insight is to think about this problem in reverse. Instead of tracking where the knight goes from the starting position, we can track how the knight could have arrived at any position.

Initially, we might think to simulate all possible paths the knight can take, but with k moves and 8 choices per move, we'd have 8^k possible paths - this becomes computationally infeasible very quickly.

The breakthrough comes from recognizing that many different paths can lead to the same position. For instance, if the knight needs to reach position (i, j) after 2 moves, there might be multiple 2-move sequences that end up there. Instead of tracking each path separately, we can just track the total probability of being at each position after each number of moves.

This leads us to dynamic programming. We build up our solution step by step:

  • After 0 moves, the knight is at its starting position with probability 1
  • After 1 move, the probability of being at any reachable position is 1/8 (since each move is equally likely)
  • After 2 moves, the probability at each position depends on the probabilities from the previous step

The clever part is working backwards in our thinking but forwards in our computation. For any position (i, j) at step h, we ask: "What are all the positions the knight could have been at in step h-1 that could reach (i, j)?" Each of those previous positions contributes 1/8 of its probability to position (i, j).

By building up these probabilities layer by layer, we avoid the exponential explosion of tracking individual paths while still accounting for all possible ways the knight can move. The final probability at position (row, column) after k moves gives us our answer.

Learn more about Dynamic Programming patterns.

Solution Approach

We implement the solution using a 3D dynamic programming array f[h][i][j] where:

  • h represents the number of moves taken (from 0 to k)
  • i and j represent the position on the board

Initialization: We start by creating a 3D array with dimensions (k+1) × n × n. For the base case when h = 0 (no moves taken yet), we set f[0][i][j] = 1 for all positions (i, j) on the board. This represents that if we're at any position with 0 moves to make, we're definitely on the board with probability 1.

State Transition: For each move h from 1 to k, we calculate the probability of being at each position (i, j) by considering all possible previous positions the knight could have come from.

The knight has 8 possible moves, represented by the displacement pairs:

  • (-2, -1), (-2, 1), (2, -1), (2, 1) - moving 2 squares vertically, 1 horizontally
  • (-1, -2), (-1, 2), (1, -2), (1, 2) - moving 2 squares horizontally, 1 vertically

The code uses pairwise((-2, -1, 2, 1, -2, 1, 2, -1, -2)) to generate these 8 pairs efficiently. For each position (i, j) at step h, we:

  1. Check all 8 possible previous positions (x, y) where x = i + a and y = j + b
  2. If the previous position (x, y) is valid (within board boundaries), we add its contribution: f[h-1][x][y] / 8
  3. The division by 8 represents the 1/8 probability of choosing this particular move

Mathematical Formula: The recurrence relation is:

f[h][i][j] = Σ (f[h-1][x][y] / 8)

where the sum is over all valid positions (x, y) that can reach (i, j) in one knight move.

Final Answer: After filling the entire DP table, f[k][row][column] gives us the probability that the knight remains on the board after exactly k moves starting from position (row, column).

The time complexity is O(k × n² × 8) = O(k × n²) since we iterate through all positions for each move and check 8 possible previous positions. The space complexity is O(k × n²) for storing the 3D DP array.

Ready to land your dream job?

Unlock your dream job with a 3-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a small example with n = 3 (3×3 board), k = 2 moves, starting at position (0, 0).

Initial Setup (h = 0): We create a 3D array f[3][3][3] and initialize f[0][i][j] = 1 for all positions:

f[0] = [[1, 1, 1],
        [1, 1, 1],
        [1, 1, 1]]

This represents that before any moves, being at any position has probability 1 (we're definitely on the board).

First Move (h = 1): For each position (i, j), we calculate the probability by checking which positions could reach it with a knight move.

Let's calculate f[1][2][1] (bottom-middle position):

  • From (0, 0): Can reach (2, 1) with move (+2, +1)
  • From (0, 2): Can reach (2, 1) with move (+2, -1)
  • From (1, 3): Would need to come from column 3 (off-board) ✗
  • From (4, 0): Would need to come from row 4 (off-board) ✗
  • ... (checking all 8 possible sources)

Only positions (0, 0) and (0, 2) can reach (2, 1), so:

f[1][2][1] = f[0][0][0]/8 + f[0][0][2]/8 = 1/8 + 1/8 = 1/4

Completing all positions for h = 1:

f[1] = [[0,    0,    0   ],
        [1/8,  0,    1/8 ],
        [1/8,  1/4,  1/8 ]]

Second Move (h = 2): Now we calculate probabilities after 2 moves. Let's compute f[2][0][0]:

Checking which positions can reach (0, 0) with a knight move:

  • From (2, 1): Can reach (0, 0) with move (-2, -1)
  • From (1, 2): Can reach (0, 0) with move (-1, -2)
  • All other positions either don't exist or can't reach (0, 0)

So:

f[2][0][0] = f[1][2][1]/8 + f[1][1][2]/8 = (1/4)/8 + (1/8)/8 = 1/32 + 1/64 = 3/64

Final Answer: After completing all calculations, f[2][0][0] = 3/64 ≈ 0.046875 is the probability that the knight remains on the board after exactly 2 moves starting from position (0, 0).

This makes intuitive sense: from (0, 0), only 2 out of 8 first moves keep the knight on board (to (2, 1) and (1, 2)). From those positions, some moves return to the board and others go off. The total probability accounts for all these paths.

Solution Implementation

1class Solution:
2    def knightProbability(self, n: int, k: int, row: int, column: int) -> float:
3        # dp[moves][r][c] represents the probability of knight staying on board
4        # after 'moves' moves starting from position (r, c)
5        dp = [[[0.0] * n for _ in range(n)] for _ in range(k + 1)]
6      
7        # Base case: with 0 moves, knight is always on board (probability = 1)
8        for r in range(n):
9            for c in range(n):
10                dp[0][r][c] = 1.0
11      
12        # All 8 possible knight moves: (row_offset, col_offset)
13        knight_moves = [
14            (-2, -1), (-2, 1), (-1, -2), (-1, 2),
15            (1, -2), (1, 2), (2, -1), (2, 1)
16        ]
17      
18        # Fill the dp table for each number of moves from 1 to k
19        for moves in range(1, k + 1):
20            for r in range(n):
21                for c in range(n):
22                    # For each position, calculate probability by summing
23                    # probabilities from all valid previous positions
24                    for row_offset, col_offset in knight_moves:
25                        prev_row = r + row_offset
26                        prev_col = c + col_offset
27                      
28                        # Check if the previous position is within the board
29                        if 0 <= prev_row < n and 0 <= prev_col < n:
30                            # Add probability from previous position divided by 8
31                            # (since knight has 8 possible moves)
32                            dp[moves][r][c] += dp[moves - 1][prev_row][prev_col] / 8.0
33      
34        # Return the probability at the starting position after k moves
35        return dp[k][row][column]
36
1class Solution {
2    public double knightProbability(int n, int k, int row, int column) {
3        // dp[moves][r][c] represents the probability of knight staying on board
4        // after 'moves' moves starting from position (r, c)
5        double[][][] dp = new double[k + 1][n][n];
6      
7        // Base case: with 0 moves, knight is definitely on board (probability = 1)
8        for (int i = 0; i < n; i++) {
9            for (int j = 0; j < n; j++) {
10                dp[0][i][j] = 1.0;
11            }
12        }
13      
14        // Knight moves in chess: 8 possible L-shaped moves
15        // Each pair of consecutive elements represents (deltaRow, deltaCol)
16        int[] directions = {-2, -1, -2, 1, -1, -2, -1, 2, 1, -2, 1, 2, 2, -1, 2, 1};
17      
18        // Calculate probability for each number of moves from 1 to k
19        for (int move = 1; move <= k; move++) {
20            // For each position on the board
21            for (int currentRow = 0; currentRow < n; currentRow++) {
22                for (int currentCol = 0; currentCol < n; currentCol++) {
23                    // Try all 8 possible knight moves
24                    for (int dir = 0; dir < 8; dir++) {
25                        int prevRow = currentRow + directions[dir * 2];
26                        int prevCol = currentCol + directions[dir * 2 + 1];
27                      
28                        // Check if the previous position is within board boundaries
29                        if (prevRow >= 0 && prevRow < n && prevCol >= 0 && prevCol < n) {
30                            // Add probability from previous position divided by 8
31                            // (since knight has 8 equally likely moves)
32                            dp[move][currentRow][currentCol] += dp[move - 1][prevRow][prevCol] / 8.0;
33                        }
34                    }
35                }
36            }
37        }
38      
39        // Return the probability of staying on board after k moves from (row, column)
40        return dp[k][row][column];
41    }
42}
43
1class Solution {
2public:
3    double knightProbability(int n, int k, int row, int column) {
4        // dp[move][i][j] represents the probability of the knight being at position (i, j) 
5        // after 'move' moves
6        double dp[k + 1][n][n];
7        memset(dp, 0, sizeof(dp));
8      
9        // Initialize: after 0 moves, the knight is at its starting position with probability 1
10        // We set all positions to 1 because we'll calculate backwards
11        for (int i = 0; i < n; ++i) {
12            for (int j = 0; j < n; ++j) {
13                dp[0][i][j] = 1.0;
14            }
15        }
16      
17        // Knight can move in 8 directions: 
18        // (-2,-1), (-2,+1), (+2,-1), (+2,+1), (-1,-2), (-1,+2), (+1,-2), (+1,+2)
19        int knightMoves[9] = {-2, -1, 2, 1, -2, 1, 2, -1, -2};
20      
21        // Calculate probability for each number of moves
22        for (int moveCount = 1; moveCount <= k; ++moveCount) {
23            // For each position on the board
24            for (int i = 0; i < n; ++i) {
25                for (int j = 0; j < n; ++j) {
26                    // Check all 8 possible previous positions the knight could have come from
27                    for (int direction = 0; direction < 8; ++direction) {
28                        int prevRow = i + knightMoves[direction];
29                        int prevCol = j + knightMoves[direction + 1];
30                      
31                        // If the previous position is valid (within board boundaries)
32                        if (prevRow >= 0 && prevRow < n && prevCol >= 0 && prevCol < n) {
33                            // Add the probability of being at the previous position divided by 8
34                            // (since knight has 8 possible moves from any position)
35                            dp[moveCount][i][j] += dp[moveCount - 1][prevRow][prevCol] / 8.0;
36                        }
37                    }
38                }
39            }
40        }
41      
42        // Return the probability of the knight being at the starting position after k moves
43        return dp[k][row][column];
44    }
45};
46
1/**
2 * Calculates the probability that a knight remains on the board after exactly k moves
3 * @param n - The size of the n x n chessboard
4 * @param k - The number of moves the knight will make
5 * @param row - The starting row position of the knight
6 * @param column - The starting column position of the knight
7 * @returns The probability that the knight remains on the board
8 */
9function knightProbability(n: number, k: number, row: number, column: number): number {
10    // Create a 3D DP array: dp[move][row][col]
11    // dp[h][i][j] represents the probability of reaching position (i, j) after h moves
12    const dp: number[][][] = Array.from({ length: k + 1 }, () =>
13        Array.from({ length: n }, () => Array(n).fill(0)),
14    );
15  
16    // Initialize: probability of being at any position after 0 moves is 1
17    // (assuming we start from a valid position)
18    for (let i = 0; i < n; ++i) {
19        for (let j = 0; j < n; ++j) {
20            dp[0][i][j] = 1;
21        }
22    }
23  
24    // Knight moves: 8 possible directions
25    // Pairs of (row offset, column offset) for each knight move
26    const knightMoves: number[] = [-2, -1, 2, 1, -2, 1, 2, -1, -2];
27  
28    // Calculate probabilities for each number of moves
29    for (let moves = 1; moves <= k; ++moves) {
30        // For each position on the board
31        for (let currentRow = 0; currentRow < n; ++currentRow) {
32            for (let currentCol = 0; currentCol < n; ++currentCol) {
33                // Check all 8 possible knight moves
34                for (let direction = 0; direction < 8; ++direction) {
35                    // Calculate the previous position that could lead to current position
36                    const previousRow: number = currentRow + knightMoves[direction];
37                    const previousCol: number = currentCol + knightMoves[direction + 1];
38                  
39                    // If the previous position is valid (within board boundaries)
40                    if (previousRow >= 0 && previousRow < n && previousCol >= 0 && previousCol < n) {
41                        // Add the probability of reaching current position from previous position
42                        // Divide by 8 since knight has 8 equally likely moves
43                        dp[moves][currentRow][currentCol] += dp[moves - 1][previousRow][previousCol] / 8;
44                    }
45                }
46            }
47        }
48    }
49  
50    // Return the probability of being at the starting position after k moves
51    return dp[k][row][column];
52}
53

Time and Space Complexity

The time complexity of this code is O(k × n^2). The analysis breaks down as follows:

  • The outermost loop iterates k times (from 1 to k)
  • For each iteration of k, there are two nested loops that each iterate n times, giving us n^2 iterations
  • Inside the innermost loops, the pairwise operation generates 8 knight move directions, which is a constant factor
  • Therefore, the total time complexity is O(k × n^2 × 8) = O(k × n^2)

The space complexity is O(k × n^2). This is because:

  • The 3D array f has dimensions (k+1) × n × n
  • This stores the probability values for each cell on the board at each step from 0 to k
  • The total space required is therefore O((k+1) × n^2) = O(k × n^2)

Here, k represents the number of moves the knight can make, and n represents the size of the n × n chessboard.

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

Common Pitfalls

1. Confusing the Direction of Dynamic Programming

The Pitfall: Many developers mistakenly interpret the DP state incorrectly. They think dp[h][i][j] represents "the probability of reaching position (i,j) after h moves starting from the initial position (row, column)". This leads to incorrect initialization and state transitions.

Why This Happens: The problem statement asks for the probability of staying on board after k moves starting from a specific position, which can be ambiguous about whether we're tracking forward from the start or backward from all positions.

The Correct Interpretation: dp[h][i][j] actually represents "the probability of staying on the board for the remaining h moves if the knight is currently at position (i,j)". This is a backward DP approach where:

  • dp[0][i][j] = 1 for all valid positions (with 0 moves left, we're definitely on board)
  • We work backwards to calculate dp[k][row][column]

2. Incorrect Previous Position Calculation

The Pitfall:

# WRONG: Subtracting offsets instead of adding
prev_row = r - row_offset  # This is incorrect!
prev_col = c - col_offset  # This is incorrect!

Why This Happens: Intuition suggests subtracting the move offset to get the previous position, but the offsets are already defined from the perspective of the previous position.

The Correct Approach:

# CORRECT: Adding offsets to get previous position
prev_row = r + row_offset
prev_col = c + col_offset

Since the knight moves are defined as displacements FROM a position, to find where the knight came FROM to reach current position (r,c), we need to ADD the offsets.

3. Space Optimization Misimplementation

The Pitfall: Trying to optimize space by using only 2D arrays but incorrectly managing the state updates:

# WRONG: Modifying the same array being read from
dp = [[1.0] * n for _ in range(n)]
for move in range(k):
    for r in range(n):
        for c in range(n):
            dp[r][c] = 0  # Clearing before we're done reading!
            # ... calculate new value

The Solution: Use two alternating 2D arrays or properly manage temporary storage:

# CORRECT: Using two arrays and swapping
prev = [[1.0] * n for _ in range(n)]
curr = [[0.0] * n for _ in range(n)]

for move in range(k):
    for r in range(n):
        for c in range(n):
            curr[r][c] = 0
            for row_offset, col_offset in knight_moves:
                prev_row = r + row_offset
                prev_col = c + col_offset
                if 0 <= prev_row < n and 0 <= prev_col < n:
                    curr[r][c] += prev[prev_row][prev_col] / 8.0
    prev, curr = curr, prev  # Swap arrays

return prev[row][column]

4. Integer Division in Python 2 vs Python 3

The Pitfall: In older Python 2 code or when habits carry over:

# Potential issue in Python 2 (not Python 3)
dp[moves][r][c] += dp[moves - 1][prev_row][prev_col] / 8

The Safe Approach: Always use float division explicitly to ensure compatibility:

dp[moves][r][c] += dp[moves - 1][prev_row][prev_col] / 8.0

5. Misunderstanding the Problem Requirements

The Pitfall: Some might think the knight stops moving once it goes off the board and count that as a valid end state, or they might think the knight can return to the board after going off.

The Correct Understanding:

  • Once the knight moves off the board, it cannot return
  • We only count cases where the knight remains on the board after ALL k moves
  • Every move has exactly 8 choices regardless of validity (the 1/8 probability applies to all 8 directions)
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Which technique can we use to find the middle of a linked list?


Recommended Readings

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

Load More