Don't Get Volunteered!
Created: Dec 16, 2021
Last modified: Jul 07, 2025
TL, DR.
Don't Get Volunteered!
======================
As a henchman on Commander Lambda's space station, you're expected to be resourceful, smart, and a quick thinker. It's not easy building a doomsday device and capturing bunnies at the same time, after all! In order to make sure that everyone working for her is sufficiently quick-witted, Commander Lambda has installed new flooring outside the henchman dormitories. It looks like a chessboard, and every morning and evening you have to solve a new movement puzzle in order to cross the floor. That would be fine if you got to be the rook or the queen, but instead, you have to be the knight. Worse, if you take too much time solving the puzzle, you get "volunteered" as a test subject for the LAMBCHOP doomsday device!
To help yourself get to and from your bunk every day, write a function called answer(src, dest) which takes in two parameters: the source square, on which you start, and the destination square, which is where you need to land to solve the puzzle. The function should return an integer representing the smallest number of moves it will take for you to travel from the source square to the destination square using a chess knight's moves (that is, two squares in any direction immediately followed by one square perpendicular to that direction, or vice versa, in an "L" shape). Both the source and destination squares will be an integer between 0 and 63, inclusive, and are numbered like the example chessboard below:
-------------------------
| 0| 1| 2| 3| 4| 5| 6| 7|
-------------------------
| 8| 9|10|11|12|13|14|15|
-------------------------
|16|17|18|19|20|21|22|23|
-------------------------
|24|25|26|27|28|29|30|31|
-------------------------
|32|33|34|35|36|37|38|39|
-------------------------
|40|41|42|43|44|45|46|47|
-------------------------
|48|49|50|51|52|53|54|55|
-------------------------
|56|57|58|59|60|61|62|63|
-------------------------
Test cases
==========
-- Python cases --
Inputs:
(int) src = 19
(int) dest = 36
Outputs:
(int) 1
Inputs:
(int) src = 0
(int) dest = 1
Output:
(int) 3
Picture an 8×8 chessboard with its squares numbered from 0 to 63.
You're the knight, you start on one square, and you need to reach another one.
The question is simple: what's the smallest number of moves it takes to get there?
If you try to work it out in your head, you'll notice it gets tricky fast: the knight jumps around in that weird "L" shape, so sometimes a square right next to you is actually three moves away. Guessing won't cut it; we need a method.
# Think of the board as a map
Here's the trick: forget it's a chess game for a second. Every square is a node (a dot on a map), and two dots are connected if the knight can jump from one to the other in a single move. Now "fewest moves" just means "shortest route on this map".
And since every jump costs exactly the same (one move), there's a classic algorithm that nails this: Breadth-First Search, or BFS for short. The idea is like dropping a stone in a pond: first you visit every square you can reach in 1 move, then every square you can reach in 2, then 3... The first time you touch the target, you know you got there the shortest way possible.
# Watch it spread
From any square, the knight has (at most) eight possible jumps. In code, we can write them as small
(dx, dy) offsets, that's the _neighbors list below. Next to it, you can see BFS in action:
each number is how many moves it takes to reach that square from the knight.
Legend:
1
2
3
4 Already visited, with their distance
Next in line (the frontier)
Being explored right now
_neighbors = [
(-2, -1), (-1, -2),
( 1, -2), ( 2, -1),
( 2, 1), ( 1, 2),
(-1, 2), (-2, 1)
]Go ahead, play with it! Drag the slider to move through the search step by step, or drag the knight somewhere else and watch how the "ripples" change. Notice how BFS always finishes every square at distance 1 before touching any at distance 2. That level-by-level order is exactly what guarantees the shortest path.
# From distances to an actual route
Knowing the distance is great, but it's even nicer to see the route. While BFS explores, it can also write down where it came from for each square. Once it reaches the target, you just follow those notes backwards and you get the path.
Try it below: drag the knight or the pawn anywhere on the board. The highlighted squares show one of the shortest routes (there's often more than one, and they're all equally good).
Right now, the knight needs at least moves to reach the pawn.
# The code
Here's my solution. A quick tour so it's easier to follow:
The squares come as numbers (0 - 63), so first we turn them into
(row, column) with n // 8 and n % 8.
Node.neighbors() applies the eight jumps and throws away the ones that land off the board.
calc_distance is the BFS itself: a queue of squares waiting to be visited, a camefrom
dictionary to remember where we came from, and a distance dictionary with the number of moves.
It stops as soon as it reaches the goal.
Foobar ran on Python 2.7; that's why you'll see import Queue instead of queue.
import Queue
_neighbors = [(-2, -1), (-1, -2),
( 1, -2), ( 2, -1),
( 2, 1), ( 1, 2),
(-1, 2), (-2, 1)]
class Node:
def __init__(self, position, came_from, size=8):
self.position = position
self.x = position[0]
self.y = position[1]
self.cameFrom = came_from
self.size = size
def __hash__(self):
return hash(self.position)
def neighbors(self):
moves = []
for i in range(len(_neighbors)):
if 0 <= _neighbors[i][0] + self.x < self.size and \
0 <= _neighbors[i][1] + self.y < self.size:
moves.append(Node(
(_neighbors[i][0] + self.x, _neighbors[i][1] + self.y),
(self.x, self.y), self.size)
)
return moves
def calc_distance(goal, start=(0, 0)):
frontier = Queue.Queue()
camefrom = dict()
distance = dict()
_start = Node(start, None)
if frontier.empty():
frontier.put(_start)
camefrom[_start.position] = None
distance[_start.position] = 0
while not frontier.empty():
current = frontier.get()
if current.position == goal:
break
for n in current.neighbors():
if n.position not in camefrom:
frontier.put(n)
camefrom[n.position] = current.position
distance[n.position] = distance[n.cameFrom] + 1
return {"cameFrom": camefrom, "distance": distance}
def solution(src, dest):
_src = (src // 8, src % 8)
_dest = (dest // 8, dest % 8)
result = calc_distance(start=_src, goal=_dest)
return result["distance"][_dest]
# Wrapping up
The big takeaway: whenever you need the fewest steps and every step costs the same, BFS is your friend. It works for knights, mazes, word puzzles, social networks... Once you learn to see a problem as a map of connected dots, a lot of "hard" puzzles suddenly become familiar.
