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