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.

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.

Listing 11 Complete source code
  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