|
| 1 | +namespace Algorithms.Problems.KnightTour; |
| 2 | + |
| 3 | +/// <summary> |
| 4 | +/// Computes a (single) Knight's Tour on an <c>n × n</c> chessboard using |
| 5 | +/// depth-first search (DFS) with backtracking. |
| 6 | +/// </summary> |
| 7 | +/// <remarks> |
| 8 | +/// <para> |
| 9 | +/// A Knight's Tour is a sequence of knight moves that visits every square exactly once. |
| 10 | +/// This implementation returns the first tour it finds (if any), starting from whichever |
| 11 | +/// starting cell leads to a solution first. It explores every board square as a potential |
| 12 | +/// starting position in row-major order. |
| 13 | +/// </para> |
| 14 | +/// <para> |
| 15 | +/// The algorithm is a plain backtracking search—no heuristics (e.g., Warnsdorff’s rule) |
| 16 | +/// are applied. As a result, runtime can grow exponentially with <c>n</c> and become |
| 17 | +/// impractical on larger boards. |
| 18 | +/// </para> |
| 19 | +/// <para> |
| 20 | +/// <b>Solvability (square boards):</b> |
| 21 | +/// A (non-closed) tour exists for <c>n = 1</c> and for all <c>n ≥ 5</c>. |
| 22 | +/// There is no tour for <c>n ∈ {2, 3, 4}</c>. This implementation throws an |
| 23 | +/// <see cref="ArgumentException"/> if no tour is found. |
| 24 | +/// </para> |
| 25 | +/// <para> |
| 26 | +/// <b>Coordinate convention:</b> The board is indexed as <c>[row, column]</c>, |
| 27 | +/// zero-based, with <c>(0,0)</c> in the top-left corner. |
| 28 | +/// </para> |
| 29 | +/// </remarks> |
| 30 | +public sealed class OpenKnightTour |
| 31 | +{ |
| 32 | + /// <summary> |
| 33 | + /// Attempts to find a Knight's Tour on an <c>n × n</c> board. |
| 34 | + /// </summary> |
| 35 | + /// <param name="n">Board size (number of rows/columns). Must be positive.</param> |
| 36 | + /// <returns> |
| 37 | + /// A 2D array of size <c>n × n</c> where each cell contains the |
| 38 | + /// 1-based visit order (from <c>1</c> to <c>n*n</c>) of the knight. |
| 39 | + /// </returns> |
| 40 | + /// <exception cref="ArgumentException"> |
| 41 | + /// Thrown when <paramref name="n"/> ≤ 0, or when no tour exists / is found for the given <paramref name="n"/>. |
| 42 | + /// </exception> |
| 43 | + /// <remarks> |
| 44 | + /// <para> |
| 45 | + /// This routine tries every square as a starting point. As soon as a complete tour is found, |
| 46 | + /// the filled board is returned. If no tour is found, an exception is thrown. |
| 47 | + /// </para> |
| 48 | + /// <para> |
| 49 | + /// <b>Performance:</b> Exponential in the worst case. For larger boards, consider adding |
| 50 | + /// Warnsdorff’s heuristic (choose next moves with the fewest onward moves) or a hybrid approach. |
| 51 | + /// </para> |
| 52 | + /// </remarks> |
| 53 | + public int[,] Tour(int n) |
| 54 | + { |
| 55 | + if (n <= 0) |
| 56 | + { |
| 57 | + throw new ArgumentException("Board size must be positive.", nameof(n)); |
| 58 | + } |
| 59 | + |
| 60 | + var board = new int[n, n]; |
| 61 | + |
| 62 | + // Try every square as a starting point. |
| 63 | + for (var r = 0; r < n; r++) |
| 64 | + { |
| 65 | + for (var c = 0; c < n; c++) |
| 66 | + { |
| 67 | + board[r, c] = 1; // first step |
| 68 | + if (KnightTourHelper(board, (r, c), 1)) |
| 69 | + { |
| 70 | + return board; |
| 71 | + } |
| 72 | + |
| 73 | + board[r, c] = 0; // backtrack and try next start |
| 74 | + } |
| 75 | + } |
| 76 | + |
| 77 | + throw new ArgumentException($"Knight Tour cannot be performed on a board of size {n}."); |
| 78 | + } |
| 79 | + |
| 80 | + /// <summary> |
| 81 | + /// Recursively extends the current partial tour from <paramref name="pos"/> after placing |
| 82 | + /// move number <paramref name="current"/> in that position. |
| 83 | + /// </summary> |
| 84 | + /// <param name="board">The board with placed move numbers; <c>0</c> means unvisited.</param> |
| 85 | + /// <param name="pos">Current knight position (<c>Row</c>, <c>Col</c>).</param> |
| 86 | + /// <param name="current">The move number just placed at <paramref name="pos"/>.</param> |
| 87 | + /// <returns><c>true</c> if a full tour is completed; <c>false</c> otherwise.</returns> |
| 88 | + /// <remarks> |
| 89 | + /// Tries each legal next move in a fixed order (no heuristics). If a move leads to a dead end, |
| 90 | + /// it backtracks by resetting the target cell to <c>0</c> and tries the next candidate. |
| 91 | + /// </remarks> |
| 92 | + private bool KnightTourHelper(int[,] board, (int Row, int Col) pos, int current) |
| 93 | + { |
| 94 | + if (IsComplete(board)) |
| 95 | + { |
| 96 | + return true; |
| 97 | + } |
| 98 | + |
| 99 | + foreach (var (nr, nc) in GetValidMoves(pos, board.GetLength(0))) |
| 100 | + { |
| 101 | + if (board[nr, nc] == 0) |
| 102 | + { |
| 103 | + board[nr, nc] = current + 1; |
| 104 | + |
| 105 | + if (KnightTourHelper(board, (nr, nc), current + 1)) |
| 106 | + { |
| 107 | + return true; |
| 108 | + } |
| 109 | + |
| 110 | + board[nr, nc] = 0; // backtrack |
| 111 | + } |
| 112 | + } |
| 113 | + |
| 114 | + return false; |
| 115 | + } |
| 116 | + |
| 117 | + /// <summary> |
| 118 | + /// Computes all legal knight moves from <paramref name="position"/> on an <c>n × n</c> board. |
| 119 | + /// </summary> |
| 120 | + /// <param name="position">Current position (<c>R</c>, <c>C</c>).</param> |
| 121 | + /// <param name="n">Board dimension (rows = columns = <paramref name="n"/>).</param> |
| 122 | + /// <returns> |
| 123 | + /// An enumeration of on-board destination coordinates. Order is fixed and unoptimized: |
| 124 | + /// <c>(+1,+2), (-1,+2), (+1,-2), (-1,-2), (+2,+1), (+2,-1), (-2,+1), (-2,-1)</c>. |
| 125 | + /// </returns> |
| 126 | + /// <remarks> |
| 127 | + /// Keeping a deterministic order makes the search reproducible, but it’s not necessarily fast. |
| 128 | + /// To accelerate, pre-sort by onward-degree (Warnsdorff) or by a custom heuristic. |
| 129 | + /// </remarks> |
| 130 | + private IEnumerable<(int R, int C)> GetValidMoves((int R, int C) position, int n) |
| 131 | + { |
| 132 | + int r = position.R, c = position.C; |
| 133 | + |
| 134 | + var candidates = new (int Dr, int Dc)[] |
| 135 | + { |
| 136 | + (1, 2), (-1, 2), (1, -2), (-1, -2), |
| 137 | + (2, 1), (2, -1), (-2, 1), (-2, -1), |
| 138 | + }; |
| 139 | + |
| 140 | + foreach (var (dr, dc) in candidates) |
| 141 | + { |
| 142 | + int nr = r + dr, nc = c + dc; |
| 143 | + if (nr >= 0 && nr < n && nc >= 0 && nc < n) |
| 144 | + { |
| 145 | + yield return (nr, nc); |
| 146 | + } |
| 147 | + } |
| 148 | + } |
| 149 | + |
| 150 | + /// <summary> |
| 151 | + /// Checks whether the tour is complete; i.e., every cell is non-zero. |
| 152 | + /// </summary> |
| 153 | + /// <param name="board">The board to check.</param> |
| 154 | + /// <returns><c>true</c> if all cells have been visited; otherwise, <c>false</c>.</returns> |
| 155 | + /// <remarks> |
| 156 | + /// A complete board means the knight has visited exactly <c>n × n</c> distinct cells. |
| 157 | + /// </remarks> |
| 158 | + private bool IsComplete(int[,] board) |
| 159 | + { |
| 160 | + var n = board.GetLength(0); |
| 161 | + for (var row = 0; row < n; row++) |
| 162 | + { |
| 163 | + for (var col = 0; col < n; col++) |
| 164 | + { |
| 165 | + if (board[row, col] == 0) |
| 166 | + { |
| 167 | + return false; |
| 168 | + } |
| 169 | + } |
| 170 | + } |
| 171 | + |
| 172 | + return true; |
| 173 | + } |
| 174 | +} |
0 commit comments