One of the most fascinating and ancient problems in recreational mathematics is the Knightâs Tour â the challenge of moving a chess knight so that it visits every square of an NĂN chessboard exactly once.
Itâs not just a puzzle: itâs a deep exploration of backtracking, graph traversal, and even heuristic search.
In this post, Iâll show how I implemented the Knightâs Tour in Haskell â first using a basic DFS (Depth-First Search), and then improved it with Warnsdorffâs rule, a clever heuristic that dramatically improves performance.
âïž The Problem
The knightâs movement pattern is unique: it moves two squares in one direction and one in the perpendicular direction.
In coordinates, from position (x, y), it can move to:
(x ± 2, y ± 1) or (x ± 1, y ± 2)
The challenge:
Find a sequence of 64 moves (for an 8Ă8 board) such that each square is visited exactly once.
- Open tour: start and end anywhere.
- Closed tour: the last square connects back to the first via a legal knight move.
đ§ Step 1 â The Depth-First Search (DFS) Approach
My first implementation was a pure DFS backtracking search. It tries every possible knight move recursively, backtracking when a move leads to a dead end.
knightsTo :: (Int, Int) -> [(Int, Int)]
knightsTo (c, r) =
let chessboard = zip ([(x,y) | x <- [1..8], y <- [1..8]]) (replicate 64 False)
newchessboard = map (\((x1, x2), alrd) -> if x1 == c && x2 == r
then ((x1, x2), True)
else ((x1, x2), alrd)) chessboard
in map fst (subKnightsTo newchessboard [((c, r), 1)])
This function tries all 8 possible moves, updating the chessboard each time. When no valid move remains, it backtracks â marking the last square as unvisited.
It works⊠but very slowly. There are billions of possible paths on an 8Ă8 board, so pure DFS can take forever.
âïž Step 2 â Enter Warnsdorffâs Rule
Warnsdorffâs heuristic is a brilliant observation from 1823:
âAlways move the knight to the square with the fewest onward moves.â
The intuition is simple:
- If you go to a square with many options, you might paint yourself into a corner later.
- If you always pick the most constrained square, you tend to leave yourself room to move.
This heuristic doesnât guarantee a solution â but it dramatically increases the chances and speed.
I combined this heuristic with DFS to create an elegant, efficient hybrid.
đ Step 3 â DFS + Warnsdorff Implementation
import qualified Data.Array as A
import Data.List (sortOn)
type Pos = (Int, Int)
type Board = A.Array Pos Bool
-- Initialize 8x8 board
initBoard :: Board
initBoard = A.array ((1,1), (8,8)) [((x,y), False) | x <- [1..8], y <- [1..8]]
-- Knight moves
knightMoves :: Pos -> [Pos]
knightMoves (c,r) = filter onBoard
[ (c+2,r+1), (c+2,r-1), (c-2,r+1), (c-2,r-1)
, (c+1,r+2), (c+1,r-2), (c-1,r+2), (c-1,r-2)
]
where onBoard (x,y) = x >= 1 && x <= 8 && y >= 1 && y <= 8
-- Warnsdorff sorting: prioritize constrained moves
sortByDegree :: Board -> [Pos] -> [Pos]
sortByDegree board = sortOn (\p -> length . filter (not . (board A.!)) $ knightMoves p)
-- Depth-first search
knightDFS :: Board -> Pos -> [Pos] -> Maybe [Pos]
knightDFS board pos path
| length path == 64 = Just (reverse path)
| otherwise =
let board' = board A.// [(pos, True)]
moves = sortByDegree board' $ filter (not . (board' A.!)) (knightMoves pos)
in tryMoves board' moves path
where
tryMoves _ [] _ = Nothing
tryMoves b (m:ms) p =
case knightDFS b m (m:p) of
Just tour -> Just tour
Nothing -> tryMoves b ms p
-- Entry point
betterKnightTo2 :: Pos -> Maybe [Pos]
betterKnightTo2 start = knightDFS initBoard start [start]
đ§© How It Works
initBoardcreates an 8Ă8 grid ofFalsevalues (unvisited squares).knightMoveslists all possible knight jumps that stay within bounds.sortByDegreeapplies Warnsdorffâs rule â sorting next moves by the number of onward moves.knightDFSrecursively explores moves, marking positions as visited.- Backtracking: if a path dead-ends, the recursion unwinds and tries the next move.
Because of the heuristic ordering, the DFS rarely has to backtrack much â in most cases, it finds a complete tour almost immediately.
đĄ Example Run
λ> betterKnightTo2 (1,1)
Just [(1,1),(3,2),(5,1),(7,2),(8,4), ... (2,3)]
It outputs one valid knightâs tour starting from (1,1). You can visualize it as a continuous path covering every cell exactly once.
đ§ź Complexity and Performance
- Naive DFS: exponential in NÂČ. Practically infeasible for 8Ă8.
- DFS + Warnsdorff: near-linear average runtime â solves in under a second.
- Pure Warnsdorff (no backtracking): even faster but may fail for certain starts.
This hybrid balances completeness (DFS ensures a solution) with efficiency (Warnsdorff guides the search).
đ Closing Thoughts
The Knightâs Tour problem is a beautiful blend of graph traversal, backtracking, and heuristic reasoning. Implementing it in Haskell highlights how cleanly recursion can express complex search logic.
"The knight moves through the board not by brute force, but by intuition â and a touch of functional elegance."
Key Takeaways
- Representing the board functionally with immutability keeps logic pure.
- Warnsdorffâs heuristic transforms a brute-force search into a guided one.
- DFS remains the foundation â heuristics just make it smarter.