1197. Minimum Knight Moves π
Problem Description
You have a knight piece on an infinite chessboard, initially positioned at coordinate [0, 0]
. The chessboard extends infinitely in all directions (from negative infinity to positive infinity).
A knight in chess moves in an "L" shape - it can move exactly 2 squares in one cardinal direction (horizontal or vertical) and then 1 square perpendicular to that direction, or 1 square in a cardinal direction and then 2 squares perpendicular. This gives the knight 8 possible moves from any position:
(-2, 1)
,(-1, 2)
,(1, 2)
,(2, 1)
(2, -1)
,(1, -2)
,(-1, -2)
,(-2, -1)
Given a target position [x, y]
, you need to find the minimum number of moves required for the knight to reach that position from its starting position [0, 0]
.
The problem guarantees that a solution always exists - the knight can always reach any position on the infinite board given enough moves.
For example, if the target is [2, 1]
, the knight can reach it in just 1 move by moving 2 squares horizontally and 1 square vertically. If the target is [5, 5]
, it would take 4 moves to reach that position.
The solution uses BFS (Breadth-First Search) to find the shortest path. Starting from [0, 0]
, it explores all positions reachable in 1 move, then all positions reachable in 2 moves, and so on, until it reaches the target position [x, y]
. The BFS approach guarantees finding the minimum number of moves since it explores positions level by level.
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 chessboard can be modeled as a graph where each position is a node, and edges connect positions that a knight can move between in one step. The knight's movement creates a graph structure.
Is it a tree?
- No: The graph is not a tree because there can be multiple paths to reach the same position, and cycles are possible (a knight can return to previously visited positions).
Is the problem related to directed acyclic graphs?
- No: The graph is not acyclic - the knight can move back and forth between positions creating cycles.
Is the problem related to shortest paths?
- Yes: We need to find the minimum number of moves (shortest path) from the starting position
[0, 0]
to the target position[x, y]
.
Is the graph Weighted?
- No: Each move of the knight counts as exactly 1 step. All edges have the same weight (unweighted graph).
BFS
- Yes: For unweighted shortest path problems, BFS is the optimal choice.
Conclusion: The flowchart correctly leads us to use BFS (Breadth-First Search) for this problem. BFS is perfect here because:
- We're dealing with an unweighted graph (each knight move = 1 step)
- We need the shortest path from source to destination
- BFS explores nodes level by level, guaranteeing the first time we reach the target is via the shortest path
Intuition
When we think about how a knight moves on a chessboard, we realize that from any position, it can reach exactly 8 different positions (assuming they're all within bounds). This creates a natural graph structure where each position is connected to 8 other positions.
The key insight is that we want the minimum number of moves, which immediately suggests thinking about shortest path algorithms. Since each knight move counts as exactly 1 step regardless of direction or distance covered, we're dealing with an unweighted graph problem.
For unweighted shortest path problems, BFS is the go-to algorithm because it explores positions level by level:
- Level 0: Starting position
[0, 0]
- Level 1: All positions reachable in 1 move
- Level 2: All positions reachable in 2 moves
- And so on...
The first time BFS encounters the target position [x, y]
, we know we've found the shortest path because BFS explores all positions at distance k
before exploring any position at distance k+1
.
The implementation uses a queue to maintain positions to explore and a visited set to avoid revisiting positions (which would create unnecessary work). Each iteration of the outer loop represents one more move, and we explore all positions reachable with the current number of moves before incrementing the move counter.
The eight possible knight moves [(-2,1), (-1,2), (1,2), (2,1), (2,-1), (1,-2), (-1,-2), (-2,-1)]
represent the "L-shaped" movement pattern. From any position (i, j)
, we simply add these offsets to get all reachable positions and continue the BFS exploration.
Learn more about Breadth-First Search patterns.
Solution Approach
The solution implements a standard BFS approach with a queue and visited set. Let's walk through the implementation details:
Data Structures Used:
- Queue (
deque
): Stores positions to explore in FIFO order, ensuring we process positions level by level - Visited Set (
vis
): Tracks already visited positions to avoid redundant exploration - Direction Array (
dirs
): Stores the 8 possible knight moves as coordinate offsets
Algorithm Implementation:
-
Initialization:
- Start with the knight at position
(0, 0)
in the queue - Mark
(0, 0)
as visited - Set move counter
ans = 0
- Start with the knight at position
-
BFS Exploration:
- Process all positions at the current level (current move count)
- For each position
(i, j)
in the current level:- Check if we've reached the target
(x, y)
- if yes, return the current move count - Generate all 8 possible next positions by adding direction offsets
- For each new position
(c, d)
:- If not visited, mark it as visited and add to queue for next level
- Check if we've reached the target
-
Level Tracking:
- The outer
while
loop continues as long as there are positions to explore - The inner
for
loop processes exactlylen(q)
positions - all positions reachable with the current number of moves - After processing each level, increment
ans
by 1
- The outer
Key Optimization Points:
The reference mentions Bidirectional BFS as an optimization technique. Instead of searching only from start to end, bidirectional BFS searches from both directions simultaneously:
- Maintain two queues:
q1
for forward search (start β end) andq2
for backward search (end β start) - Maintain two hash maps:
m1
andm2
to record visited nodes and their distances - Always expand the smaller queue first to minimize the search space
- When a position is found that's been visited from the opposite direction, the shortest path is found (sum of distances from both directions)
- If either queue becomes empty, no path exists
The bidirectional approach significantly reduces the search space, especially when the target is far from the origin, as it explores roughly 2 Γ sqrt(n)
nodes instead of n
nodes in the worst case.
Time Complexity: O(|x| Γ |y|)
- In the worst case, we might need to explore positions in a region proportional to the target coordinates
Space Complexity: O(|x| Γ |y|)
- For storing visited positions in the set
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through finding the minimum moves for a knight to reach position [2, 3]
from [0, 0]
.
Initial Setup:
- Start:
[0, 0]
- Target:
[2, 3]
- Queue:
[[0, 0]]
- Visited:
{(0, 0)}
- Moves:
0
Move 1 - Explore from [0, 0]:
From [0, 0]
, the knight can reach 8 positions:
[0, 0] + [-2, 1] = [-2, 1]
β Add to queue[0, 0] + [-1, 2] = [-1, 2]
β Add to queue[0, 0] + [1, 2] = [1, 2]
β Add to queue[0, 0] + [2, 1] = [2, 1]
β Add to queue[0, 0] + [2, -1] = [2, -1]
β Add to queue[0, 0] + [1, -2] = [1, -2]
β Add to queue[0, 0] + [-1, -2] = [-1, -2]
β Add to queue[0, 0] + [-2, -1] = [-2, -1]
β Add to queue
Queue after Move 1: [[-2,1], [-1,2], [1,2], [2,1], [2,-1], [1,-2], [-1,-2], [-2,-1]]
None match target [2, 3]
, continue...
Move 2 - Explore all positions reachable in 1 move:
Process each position from Move 1. Let's focus on [1, 2]
which is close to our target:
From [1, 2]
, applying the 8 moves:
[1, 2] + [-2, 1] = [-1, 3]
[1, 2] + [-1, 2] = [0, 4]
[1, 2] + [1, 2] = [2, 4]
[1, 2] + [2, 1] = [3, 3]
[1, 2] + [2, -1] = [3, 1]
[1, 2] + [1, -2] = [2, 0]
[1, 2] + [-1, -2] = [0, 0]
(already visited, skip)[1, 2] + [-2, -1] = [-1, 1]
Similarly, from [2, 1]
:
[2, 1] + [-2, 1] = [0, 2]
[2, 1] + [-1, 2] = [1, 3]
[2, 1] + [1, 2] = [3, 3]
(might be duplicate)[2, 1] + [2, 1] = [4, 2]
[2, 1] + [2, -1] = [4, 0]
[2, 1] + [1, -2] = [3, -1]
[2, 1] + [-1, -2] = [1, -1]
[2, 1] + [-2, -1] = [0, 0]
(already visited, skip)
After processing all 8 positions from Move 1, none match [2, 3]
.
Move 3 - Explore all positions reachable in 2 moves:
Continue BFS. When processing position [0, 2]
(reachable in 2 moves):
From [0, 2]
:
[0, 2] + [-2, 1] = [-2, 3]
[0, 2] + [-1, 2] = [-1, 4]
[0, 2] + [1, 2] = [1, 4]
[0, 2] + [2, 1] = [2, 3]
β TARGET FOUND!
The BFS finds [2, 3]
after 3 moves, so the minimum number of moves is 3.
Path visualization:
One optimal path: [0, 0]
β [2, 1]
β [0, 2]
β [2, 3]
This demonstrates how BFS systematically explores all positions level by level, guaranteeing the first time we reach the target is via the shortest path.
Solution Implementation
1from collections import deque
2from typing import Set, Tuple
3
4class Solution:
5 def minKnightMoves(self, x: int, y: int) -> int:
6 # Initialize BFS queue with starting position (0, 0)
7 queue = deque([(0, 0)])
8
9 # Track the number of moves (BFS levels)
10 moves = 0
11
12 # Set to keep track of visited positions to avoid revisiting
13 visited: Set[Tuple[int, int]] = {(0, 0)}
14
15 # All 8 possible knight moves: 2 squares in one direction, 1 square perpendicular
16 knight_directions = (
17 (-2, 1), (-1, 2), (1, 2), (2, 1),
18 (2, -1), (1, -2), (-1, -2), (-2, -1)
19 )
20
21 # BFS to find minimum moves to target
22 while queue:
23 # Process all positions at current level
24 level_size = len(queue)
25
26 for _ in range(level_size):
27 current_x, current_y = queue.popleft()
28
29 # Check if we've reached the target position
30 if (current_x, current_y) == (x, y):
31 return moves
32
33 # Explore all 8 possible knight moves from current position
34 for dx, dy in knight_directions:
35 next_x = current_x + dx
36 next_y = current_y + dy
37
38 # Add unvisited positions to queue
39 if (next_x, next_y) not in visited:
40 visited.add((next_x, next_y))
41 queue.append((next_x, next_y))
42
43 # Increment move count after processing each level
44 moves += 1
45
46 # Should never reach here for valid inputs
47 return -1
48
1class Solution {
2 public int minKnightMoves(int x, int y) {
3 // Offset coordinates to avoid negative indices (knight starts at origin)
4 // Using 310 as offset to handle the constraint range [-300, 300]
5 x += 310;
6 y += 310;
7
8 // Initialize the number of moves
9 int moves = 0;
10
11 // BFS queue to store positions to explore
12 Queue<int[]> queue = new ArrayDeque<>();
13 queue.offer(new int[] {310, 310}); // Starting position (0,0) with offset
14
15 // Visited array to track explored positions
16 boolean[][] visited = new boolean[700][700]; // 700x700 grid to cover all possible positions
17 visited[310][310] = true; // Mark starting position as visited
18
19 // Eight possible knight moves: L-shaped moves
20 int[][] directions = {
21 {-2, 1}, {-1, 2}, {1, 2}, {2, 1}, // Upper-right quadrant moves
22 {2, -1}, {1, -2}, {-1, -2}, {-2, -1} // Lower-left quadrant moves
23 };
24
25 // BFS to find minimum moves
26 while (!queue.isEmpty()) {
27 // Process all positions at current distance level
28 int levelSize = queue.size();
29 for (int i = 0; i < levelSize; i++) {
30 int[] currentPosition = queue.poll();
31
32 // Check if we reached the target position
33 if (currentPosition[0] == x && currentPosition[1] == y) {
34 return moves;
35 }
36
37 // Explore all eight possible knight moves from current position
38 for (int[] direction : directions) {
39 int nextX = currentPosition[0] + direction[0];
40 int nextY = currentPosition[1] + direction[1];
41
42 // If position hasn't been visited, mark it and add to queue
43 if (!visited[nextX][nextY]) {
44 visited[nextX][nextY] = true;
45 queue.offer(new int[] {nextX, nextY});
46 }
47 }
48 }
49 // Increment moves after processing each level
50 moves++;
51 }
52
53 // Should never reach here as solution always exists
54 return -1;
55 }
56}
57
1class Solution {
2public:
3 int minKnightMoves(int x, int y) {
4 // Offset coordinates to handle negative values
5 // Adding 310 ensures all coordinates are positive (safe range for -300 to 300)
6 x += 310;
7 y += 310;
8
9 // Initialize the minimum number of moves
10 int minMoves = 0;
11
12 // BFS queue to store positions to explore
13 queue<pair<int, int>> bfsQueue;
14 bfsQueue.push({310, 310}); // Start from origin (0,0) with offset
15
16 // Visited array to track explored positions
17 vector<vector<bool>> visited(700, vector<bool>(700, false));
18 visited[310][310] = true; // Mark starting position as visited
19
20 // Eight possible knight moves: 2 squares in one direction, 1 square perpendicular
21 vector<vector<int>> knightMoves = {
22 {-2, 1}, {-1, 2}, {1, 2}, {2, 1},
23 {2, -1}, {1, -2}, {-1, -2}, {-2, -1}
24 };
25
26 // BFS to find shortest path
27 while (!bfsQueue.empty()) {
28 // Process all positions at current distance level
29 int levelSize = bfsQueue.size();
30 for (int i = 0; i < levelSize; ++i) {
31 // Get current position from queue
32 auto [currentX, currentY] = bfsQueue.front();
33 bfsQueue.pop();
34
35 // Check if we've reached the target position
36 if (currentX == x && currentY == y) {
37 return minMoves;
38 }
39
40 // Explore all eight possible knight moves from current position
41 for (const auto& move : knightMoves) {
42 int nextX = currentX + move[0];
43 int nextY = currentY + move[1];
44
45 // If position hasn't been visited, mark it and add to queue
46 if (!visited[nextX][nextY]) {
47 visited[nextX][nextY] = true;
48 bfsQueue.push({nextX, nextY});
49 }
50 }
51 }
52 // Increment move count after processing all positions at current level
53 ++minMoves;
54 }
55
56 // Should never reach here as solution always exists
57 return -1;
58 }
59};
60
1function minKnightMoves(x: number, y: number): number {
2 // Offset coordinates to handle negative values
3 // Adding 310 ensures all coordinates are positive (safe range for -300 to 300)
4 x += 310;
5 y += 310;
6
7 // Initialize the minimum number of moves
8 let minMoves = 0;
9
10 // BFS queue to store positions to explore
11 const bfsQueue: [number, number][] = [];
12 bfsQueue.push([310, 310]); // Start from origin (0,0) with offset
13
14 // Visited array to track explored positions
15 const visited: boolean[][] = Array(700).fill(null).map(() => Array(700).fill(false));
16 visited[310][310] = true; // Mark starting position as visited
17
18 // Eight possible knight moves: 2 squares in one direction, 1 square perpendicular
19 const knightMoves: [number, number][] = [
20 [-2, 1], [-1, 2], [1, 2], [2, 1],
21 [2, -1], [1, -2], [-1, -2], [-2, -1]
22 ];
23
24 // BFS to find shortest path
25 while (bfsQueue.length > 0) {
26 // Process all positions at current distance level
27 const levelSize = bfsQueue.length;
28 for (let i = 0; i < levelSize; i++) {
29 // Get current position from queue
30 const [currentX, currentY] = bfsQueue.shift()!;
31
32 // Check if we've reached the target position
33 if (currentX === x && currentY === y) {
34 return minMoves;
35 }
36
37 // Explore all eight possible knight moves from current position
38 for (const move of knightMoves) {
39 const nextX = currentX + move[0];
40 const nextY = currentY + move[1];
41
42 // If position hasn't been visited, mark it and add to queue
43 if (!visited[nextX][nextY]) {
44 visited[nextX][nextY] = true;
45 bfsQueue.push([nextX, nextY]);
46 }
47 }
48 }
49 // Increment move count after processing all positions at current level
50 minMoves++;
51 }
52
53 // Should never reach here as solution always exists
54 return -1;
55}
56
Time and Space Complexity
Time Complexity: O(max(|x|, |y|)^2)
The algorithm uses BFS to find the shortest path from (0, 0)
to (x, y)
. In the worst case, the knight needs to explore a region roughly bounded by the target coordinates. Since a knight moves in an L-shape (2 squares in one direction and 1 square perpendicular), the maximum distance from the origin is approximately max(|x|, |y|)
. The BFS will explore nodes in concentric "rings" around the origin, and the number of nodes within a distance d
from the origin is proportional to d^2
. Therefore, the time complexity is O(max(|x|, |y|)^2)
.
Space Complexity: O(max(|x|, |y|)^2)
The space complexity is dominated by:
- The
vis
set which stores all visited positions to avoid revisiting them - The queue
q
which at most contains all nodes at a particular distance level
In the worst case, both data structures can contain O(max(|x|, |y|)^2)
positions, as the algorithm may need to explore all reachable positions within the bounded region before finding the target. The visited set grows continuously as we explore new positions, potentially storing all positions within the search area.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Memory Limit Exceeded for Large Targets
The naive BFS approach can consume excessive memory when the target coordinates are large (e.g., x=300, y=300
). The visited set grows exponentially as we explore outward, potentially storing millions of positions.
Solution: Exploit symmetry and limit the search space:
- Due to the board's symmetry, we can work with absolute values:
x = abs(x), y = abs(y)
- Restrict exploration to a bounded region around the origin and target
- Positions beyond
[-2, target_x + 2] Γ [-2, target_y + 2]
are unlikely to be part of the shortest path
2. Time Limit Exceeded for Distant Targets
Standard BFS explores positions radially outward, visiting many irrelevant positions when the target is far from the origin.
Solution: Implement Bidirectional BFS:
def minKnightMovesBidirectional(self, x: int, y: int) -> int:
x, y = abs(x), abs(y) # Use symmetry
if (x, y) == (0, 0):
return 0
# Two queues for bidirectional search
forward = {(0, 0): 0}
backward = {(x, y): 0}
knight_moves = [(2,1), (1,2), (-1,2), (-2,1),
(-2,-1), (-1,-2), (1,-2), (2,-1)]
while forward and backward:
# Always expand the smaller frontier
if len(forward) <= len(backward):
current = forward
opposite = backward
else:
current = backward
opposite = forward
next_level = {}
for (cx, cy), dist in current.items():
for dx, dy in knight_moves:
nx, ny = cx + dx, cy + dy
if (nx, ny) in opposite:
return dist + 1 + opposite[(nx, ny)]
if -2 <= nx <= x + 2 and -2 <= ny <= y + 2:
if (nx, ny) not in current:
next_level[(nx, ny)] = dist + 1
if current is forward:
forward = next_level
else:
backward = next_level
return -1
3. Not Handling Edge Cases Properly
Forgetting to handle special cases like when the target is already at the origin (0, 0)
.
Solution: Add explicit checks at the beginning:
if x == 0 and y == 0: return 0
4. Inefficient Position Representation
Using tuples in the visited set can be slower for very large searches due to tuple creation overhead.
Solution: For extremely performance-critical scenarios, consider using a more efficient representation:
# Use a single integer to represent positions
def encode(x, y):
# Offset to handle negative coordinates
offset = 1000
return (x + offset) * 10000 + (y + offset)
visited = set()
visited.add(encode(0, 0))
Which two pointer techniques do you use to check if a string is a palindrome?
Recommended Readings
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Donβt Miss This!