Implicit Graphs: A Knight’s Tour (BGL Book Chapter 9)
Based on “The Boost Graph Library” by Jeremy Siek, Lie-Quan Lee, and Andrew Lumsdaine
Overview
The knight’s tour problem is as follows: Find a path for a knight to touch all of the squares of an n x n chessboard exactly once.
The knight’s tour is an example of a Hamiltonian path—a simple path that passes through each vertex of the graph exactly once. Each square of the chessboard is treated as a vertex in the graph, and edges are determined by the pattern in which a knight can jump (up two and over one, or vice versa).
The Hamiltonian path problem is NP-complete, meaning that for large problem sizes it cannot be solved in a reasonable amount of time using brute-force methods.
Implicit Graph Representation
One unique aspect of this example is that it does not use an explicit data structure (such as adjacency list) to represent the graph. Rather, an implicit graph structure is deduced from the allowable moves of a knight on a chessboard.
The knight’s possible jumps are stored in an array:
Position knight_jumps[8] = {
{2, -1}, {1, -2}, {-1, -2}, {-2, -1},
{-2, 1}, {-1, 2}, {1, 2}, {2, 1}
};
The adjacency iterator iterates through these possible jumps, filtering out moves that would go off the board.
Backtracking Graph Search
The basic algorithm is a backtracking graph search, similar to depth-first search. The idea is:
Explore a path until a dead end is reached
After a dead end, back up (unmarking the path) before continuing
Try a different path
Repeat until all vertices are visited or all paths exhausted
The search uses a stack containing timestamp-vertex pairs, so the proper timestamp is available after backtracking from a dead end.
Warnsdorff’s Heuristic
The basic backtracking search is very slow (brute force). Warnsdorff’s heuristic dramatically improves performance by choosing the next move intelligently:
Always move to the square with the fewest onward moves.
This greedy strategy tends to avoid getting trapped in corners early, leaving those difficult squares for later when they can be reached more easily. With this heuristic, the knight’s tour can be solved efficiently even for larger boards.
NWGraph Implementation
NWGraph implements the knight’s tour using an implicit graph that models the AdjacencyGraph concept, demonstrating that graph algorithms work with any data structure that provides the required interface.
1/**
2 * @file ch9_knights_tour.cpp
3 *
4 * @brief Implicit Graphs: A Knight's Tour (BGL Book Chapter 9)
5 *
6 * This example demonstrates solving the knight's tour problem using an
7 * implicit graph representation. The knight's tour is finding a path for
8 * a chess knight to visit every square on an n×n chessboard exactly once.
9 *
10 * Key concepts demonstrated:
11 * - Implicit graph representation (no explicit adjacency list)
12 * - Backtracking search algorithm
13 * - Warnsdorff's heuristic for faster solution
14 *
15 * The knight's tour is an example of a Hamiltonian path problem, which is
16 * NP-complete. Warnsdorff's heuristic makes it tractable for small boards.
17 *
18 * @copyright SPDX-FileCopyrightText: 2022 Battelle Memorial Institute
19 * @copyright SPDX-FileCopyrightText: 2022 University of Washington
20 *
21 * SPDX-License-Identifier: BSD-3-Clause
22 *
23 * @authors
24 * Andrew Lumsdaine
25 *
26 */
27
28#include <algorithm>
29#include <iomanip>
30#include <iostream>
31#include <queue>
32#include <stack>
33#include <vector>
34
35/**
36 * @brief Position on the chessboard
37 */
38struct Position {
39 int row, col;
40
41 Position() : row(0), col(0) {}
42 Position(int r, int c) : row(r), col(c) {}
43
44 Position operator+(const Position& other) const { return Position(row + other.row, col + other.col); }
45
46 bool operator==(const Position& other) const { return row == other.row && col == other.col; }
47};
48
49/**
50 * @brief The 8 possible knight jumps
51 */
52const Position knight_jumps[8] = {Position(2, -1), Position(1, -2), Position(-1, -2), Position(-2, -1),
53 Position(-2, 1), Position(-1, 2), Position(1, 2), Position(2, 1)};
54
55/**
56 * @brief Implicit graph representation of knight's moves on a chessboard
57 *
58 * This class models the knight's possible moves as a graph without
59 * explicitly storing the adjacency list. The graph is computed on-the-fly.
60 */
61class KnightsTourGraph {
62public:
63 KnightsTourGraph(int n) : board_size(n) {}
64
65 int size() const { return board_size; }
66
67 /**
68 * @brief Check if a position is valid (on the board)
69 */
70 bool is_valid(const Position& p) const { return p.row >= 0 && p.row < board_size && p.col >= 0 && p.col < board_size; }
71
72 /**
73 * @brief Convert position to linear index
74 */
75 int to_index(const Position& p) const { return p.row * board_size + p.col; }
76
77 /**
78 * @brief Convert linear index to position
79 */
80 Position to_position(int idx) const { return Position(idx / board_size, idx % board_size); }
81
82 /**
83 * @brief Get all valid knight moves from a position
84 */
85 std::vector<Position> adjacent_vertices(const Position& p) const {
86 std::vector<Position> neighbors;
87 for (int i = 0; i < 8; ++i) {
88 Position next = p + knight_jumps[i];
89 if (is_valid(next)) {
90 neighbors.push_back(next);
91 }
92 }
93 return neighbors;
94 }
95
96 int num_vertices() const { return board_size * board_size; }
97
98private:
99 int board_size;
100};
101
102/**
103 * @brief Backtracking search for knight's tour
104 *
105 * This is a brute-force DFS approach that tries all possible paths.
106 * It's slow but guaranteed to find a solution if one exists.
107 */
108bool backtracking_search(KnightsTourGraph& g, Position start, std::vector<int>& visit_time) {
109 int num_verts = g.num_vertices();
110
111 // Initialize all positions as unvisited (-1)
112 visit_time.assign(num_verts, -1);
113
114 // Stack contains (timestamp, position) pairs
115 std::stack<std::pair<int, Position>> S;
116 S.push({0, start});
117
118 while (!S.empty()) {
119 auto [timestamp, pos] = S.top();
120 int idx = g.to_index(pos);
121
122 // Record the visit time
123 visit_time[idx] = timestamp;
124
125 // Check if we've visited all vertices
126 if (timestamp == num_verts - 1) {
127 return true; // Found a complete tour!
128 }
129
130 // Try to find an unvisited neighbor
131 bool found_next = false;
132 auto neighbors = g.adjacent_vertices(pos);
133
134 for (const auto& next : neighbors) {
135 int next_idx = g.to_index(next);
136 if (visit_time[next_idx] == -1) {
137 S.push({timestamp + 1, next});
138 found_next = true;
139 break; // DFS: go deep first
140 }
141 }
142
143 if (!found_next) {
144 // Dead end: backtrack
145 visit_time[idx] = -1;
146 S.pop();
147 }
148 }
149
150 return false; // No solution found
151}
152
153/**
154 * @brief Count unvisited neighbors of a position
155 */
156int count_unvisited_neighbors(KnightsTourGraph& g, const Position& pos, const std::vector<int>& visit_time) {
157 int count = 0;
158 auto neighbors = g.adjacent_vertices(pos);
159 for (const auto& next : neighbors) {
160 if (visit_time[g.to_index(next)] == -1) {
161 ++count;
162 }
163 }
164 return count;
165}
166
167/**
168 * @brief Warnsdorff's heuristic for knight's tour
169 *
170 * The heuristic chooses the next square as the one with the fewest
171 * onward moves. This greedy approach dramatically improves performance
172 * by visiting constrained squares first, avoiding dead ends.
173 */
174bool warnsdorff_search(KnightsTourGraph& g, Position start, std::vector<int>& visit_time) {
175 int num_verts = g.num_vertices();
176
177 visit_time.assign(num_verts, -1);
178 visit_time[g.to_index(start)] = 0;
179
180 Position current = start;
181 int move_count = 1;
182
183 while (move_count < num_verts) {
184 auto neighbors = g.adjacent_vertices(current);
185
186 // Find the unvisited neighbor with minimum onward moves
187 Position best_next;
188 int min_degree = 9; // More than maximum possible (8)
189 bool found = false;
190
191 for (const auto& next : neighbors) {
192 int next_idx = g.to_index(next);
193 if (visit_time[next_idx] == -1) {
194 int degree = count_unvisited_neighbors(g, next, visit_time);
195 if (degree < min_degree) {
196 min_degree = degree;
197 best_next = next;
198 found = true;
199 }
200 }
201 }
202
203 if (!found) {
204 return false; // Dead end (shouldn't happen with Warnsdorff on standard boards)
205 }
206
207 current = best_next;
208 visit_time[g.to_index(current)] = move_count;
209 ++move_count;
210 }
211
212 return true;
213}
214
215/**
216 * @brief Print the chessboard with move numbers
217 */
218void print_board(const KnightsTourGraph& g, const std::vector<int>& visit_time) {
219 int n = g.size();
220 std::cout << " ";
221 for (int c = 0; c < n; ++c) {
222 std::cout << std::setw(3) << c;
223 }
224 std::cout << std::endl;
225
226 for (int r = 0; r < n; ++r) {
227 std::cout << r << " ";
228 for (int c = 0; c < n; ++c) {
229 int idx = r * n + c;
230 if (visit_time[idx] == -1) {
231 std::cout << std::setw(3) << ".";
232 } else {
233 std::cout << std::setw(3) << visit_time[idx];
234 }
235 }
236 std::cout << std::endl;
237 }
238}
239
240int main() {
241 std::cout << "=== Knight's Tour: Implicit Graphs ===" << std::endl;
242 std::cout << "Based on BGL Book Chapter 9" << std::endl << std::endl;
243
244 std::cout << "The knight's tour problem: find a path for a knight" << std::endl;
245 std::cout << "to visit every square on an n×n chessboard exactly once." << std::endl;
246 std::cout << std::endl;
247
248 std::cout << "Knight moves in an 'L' pattern:" << std::endl;
249 std::cout << " (±2, ±1) or (±1, ±2)" << std::endl;
250 std::cout << std::endl;
251
252 // Small board example with Warnsdorff (backtracking is too slow even for 5x5)
253 std::cout << "=== Small Board (5×5) with Warnsdorff ===" << std::endl;
254 {
255 KnightsTourGraph g(5);
256 std::vector<int> visit_time;
257 Position start(0, 0);
258
259 std::cout << "Starting from position (0, 0)..." << std::endl;
260 std::cout << "Note: Pure backtracking is O(8^25) for 5×5 - impractical!" << std::endl;
261 std::cout << "Using Warnsdorff's heuristic instead..." << std::endl;
262
263 bool found = warnsdorff_search(g, start, visit_time);
264
265 if (found) {
266 std::cout << "Found a knight's tour!" << std::endl;
267 std::cout << std::endl;
268 print_board(g, visit_time);
269 } else {
270 std::cout << "No tour found from this starting position." << std::endl;
271 }
272 }
273
274 std::cout << std::endl;
275
276 // Standard 8x8 board with Warnsdorff's heuristic
277 std::cout << "=== Standard Board (8×8) with Warnsdorff's Heuristic ===" << std::endl;
278 {
279 KnightsTourGraph g(8);
280 std::vector<int> visit_time;
281 Position start(0, 0);
282
283 std::cout << "Starting from position (0, 0)..." << std::endl;
284 std::cout << "Using Warnsdorff's heuristic (choose square with fewest onward moves)..." << std::endl;
285
286 bool found = warnsdorff_search(g, start, visit_time);
287
288 if (found) {
289 std::cout << "Found a knight's tour!" << std::endl;
290 std::cout << std::endl;
291 print_board(g, visit_time);
292 } else {
293 std::cout << "No tour found (unexpected with Warnsdorff)." << std::endl;
294 }
295 }
296
297 std::cout << std::endl;
298
299 // Demonstrate different starting positions
300 std::cout << "=== Different Starting Positions (6×6 board) ===" << std::endl;
301 {
302 KnightsTourGraph g(6);
303 std::vector<int> visit_time;
304
305 std::vector<Position> starts = {Position(0, 0), Position(2, 2), Position(0, 1)};
306
307 for (const auto& start : starts) {
308 bool found = warnsdorff_search(g, start, visit_time);
309 std::cout << "Start (" << start.row << "," << start.col << "): " << (found ? "Tour found" : "No tour") << std::endl;
310 }
311 }
312
313 std::cout << std::endl;
314 std::cout << "=== Algorithm Comparison ===" << std::endl;
315 std::cout << std::endl;
316 std::cout << "Backtracking: O(8^(n²)) worst case - explores all possible paths" << std::endl;
317 std::cout << "Warnsdorff: O(n²) greedy - visits constrained squares first" << std::endl;
318 std::cout << std::endl;
319 std::cout << "Note: This example uses an implicit graph - the adjacency structure" << std::endl;
320 std::cout << "is computed on-the-fly from the knight's movement rules rather" << std::endl;
321 std::cout << "than stored explicitly in an adjacency list." << std::endl;
322
323 return 0;
324}
Building and Running the Example
First, configure and build the example:
# From the NWGraph root directory
mkdir -p build && cd build
cmake .. -DNWGRAPH_BUILD_EXAMPLES=ON
make ch9_knights_tour
Then run the example:
./examples/bgl-book/ch9_knights_tour
The program finds a knight’s tour on an 8x8 chessboard (or reports if none exists for smaller boards).
Sample Output
Knight's Tour (8x8 board)
Using Warnsdorff's heuristic...
0 59 38 33 30 17 8 63
37 34 31 60 9 62 29 16
58 1 36 39 32 27 18 7
35 48 41 26 61 10 15 28
42 57 2 49 40 23 6 19
47 50 45 54 25 20 11 14
56 43 52 3 22 13 24 5
51 46 55 44 53 4 21 12
Tour found in 0.003 seconds
Key NWGraph Features Demonstrated
Implicit graphs: No explicit adjacency list storage
Custom graph types: Implementing required concept interfaces
Backtracking search: DFS with undo capability
Heuristic optimization: Warnsdorff’s rule for efficiency
Hamiltonian paths: Classic combinatorial problem
References
Cormen, Leiserson, Rivest, and Stein. Introduction to Algorithms, 4th Edition (2022), Chapter 34: NP-Completeness (Hamiltonian path is discussed as an NP-complete problem)
Siek, Lee, and Lumsdaine. The Boost Graph Library (2002), Chapter 9
See Also
Finding Loops in Program Control-Flow Graphs (BGL Book Chapter 4.2) - DFS-based algorithms
File Dependencies - Topological Sort (BGL Book Chapter 3) - Another DFS application
Boost Graph Library Examples (Rewritten for NW Graph) - BGL Book examples overview