The Knight's Tour is a mathematical problem involving a knight on a chessboard. The knight is placed on the empty board and, moving according to the rules of chess, must visit each square exactly once.
It was easy to understand, easy to try, but not so easy to accomplish. And it only required a pen and a piece of paper. Whenever I managed to complete all 64 squares (by shear luck), I always felt a surge of exhilaration.
Nowadays, I've more or less abandoned chess... But strangely enough, I remembered my childhood refuge once again today, just by seeing some Sudoku puzzle in a magazine... and decided to take another swing at it. With much better tools than pen and paper :‑)
#!/usr/bin/env python import sys g_sqSize = -1 # the board size, passed at runtime g_board = [] # the board will be constructed as a list of lists def main(): global g_sqSize if len(sys.argv) != 2: g_sqSize = 8 # Default: Fill the normal 8x8 chess board else: try: g_sqSize = int(sys.argv[1]) # or, the NxN the user wants except: print("Usage: " + sys.argv[0] + " <squareSize>") sys.exit(1) for i in range(0, g_sqSize): g_board.append(g_sqSize*[0]) # Fill the board with zeroes Fill(0,0,1) # Start the recursion with a 1 in the upper left print("No solution found") # if the recursion returns, it failed def InRangeAndEmpty(ty,tx): # check if coordinates are within the board return ty>=0 and tx>=0 and ty<g_sqSize and tx<g_sqSize \ and g_board[ty][tx] == 0 # and the square is empty def Fill(y,x,counter): # The recursive function that fills the board assert g_board[y][x] == 0 g_board[y][x] = counter # Fill the square if counter == g_sqSize*g_sqSize: # Was this the last empty square? PrintBoard() # Yes, print the board... sys.exit(1) # ...and exit jumps = ((-2,1),(-1,2),(1,2),(2,1),(2,-1),(1,-2),(-1,-2),(-2,-1)) for jump in jumps: # otherwise, try all the empty neighbours in turn ty,tx = y+jump[0], x+jump[1] if InRangeAndEmpty(ty,tx): Fill(ty,tx,counter+1) # *** RECURSION! *** g_board[y][x] = 0 # if we get here, all the neighbours failed, # so reset the square and return def PrintBoard(): # print the board using nice ASCII art ('+' and '-') scale = len(str(g_sqSize*g_sqSize)) print(g_sqSize*("+" + scale*"-") + "+") for line in g_board: for elem in line: sys.stdout.write("|%*d" % (scale,elem)) print("|\n"+g_sqSize*("+" + scale*"-") + "+") if __name__ == "__main__": main()
This naive approach is functional, but...
bash$ ./knight1.py 5 +--+--+--+--+--+ | 1|20|17|12| 3| +--+--+--+--+--+ |16|11| 2| 7|18| +--+--+--+--+--+ |21|24|19| 4|13| +--+--+--+--+--+ |10|15| 6|23| 8| +--+--+--+--+--+ |25|22| 9|14| 5| +--+--+--+--+--+...only for small boards. It solves the 5x5 and 6x6 boards in less than a second, but it takes 96 seconds to solve the 7x7 and 28 seconds to solve the 8x8... I didn't even try with larger board sizes, since it was clear that naive recursion is too slow.
I then proceeded to add some extra intelligence in the recursion, by filling the "lonesome" squares first:
... emptyNeighbours = [] # find our empty neighbours for the recursion jumps = ((-2,1),(-1,2),(1,2),(2,1),(2,-1),(1,-2),(-1,-2),(-2,-1)) for jump in jumps: ty,tx = y + jump[0], x + jump[1] if InRangeAndEmpty(ty,tx): emptyNeighbours.append([ty,tx]) # recurse using our neighbours, trying first the ones with the # least amount of free neighbours, i.e. the "loners" for ty,tx in sorted(emptyNeighbours, key=lambda c: reduce( lambda x,y: x+y, map(lambda j: InRangeAndEmpty(c[0]+j[0], c[1]+j[1]) and 1 or 0, jumps))): Fill(ty,tx,counter+1) ...
Python also offers the sum function to implement the accumulation process, and it also offers list comprehensions to ease up the syntax further:
for ty,tx in sorted(emptyNeighbours, key=lambda c: sum([InRangeAndEmpty(c[0]+j[0], c[1]+j[1]) and 1 or 0 for j in jumps])): Fill(ty,tx,counter+1) ...
By using this simple policy (which I clearly remember I was also following when I was a kid!), the solving time became less than 100 milliseconds, for all square sizes up to 31x31! Beyond 31x31, at least on my manually-compiled Python interpreter, the program run out of stack space and required an increase in Python's recursion limit (via sys.setrecursionlimit). After that little tweak, the 100x100 problem was solved just as quick :‑)
P.S. Not to be paternalistic, but allow me to suggest writing this sorting code in C (using qsort?), or in C++ using STL's sort algorithm. When you finish, look at the code you wrote, and then look back at the Python code in its final form: 2 lines to implement the sorting! You'll have to agree that writing functions just to pass to qsort (or functors in order to pass to std::sort) is... well, far behind. Python syntax has tremendous advantages... Advantages that have nothing to do with libraries, and can be traced back to the combination of (a) functional programming concepts and (b) the perfect syntax that Python offers. I would truly be amazed to see anyone writing the same sorting logic in C++ in anything less than 3 times the lines of code I wrote for it in Python. And even if this is somehow possible (using external libraries like BOOST, I'd wager), the sorting code will take longer to write, and it will be far more difficult to comprehend or refactor...
It took me less than an hour to write this, and I'm a "casual user" of Python, not a Python expert...
bash$ ./knight2.py 31 +---+---+---+---+---+---+---+---+---+ ... +---+---+---+---+---+---+---+---+---+ | 1|754| 65| 72| 3|756| 77| 80| 5| |587|478| 13| 90|341|476| 15| 92| 95| +---+---+---+---+---+---+---+---+---+ +---+---+---+---+---+---+---+---+---+ | 66| 71| 2|755| 76| 73| 4|887|762| | 12| 89|528|477| 14| 91| 94|335| 16| +---+---+---+---+---+---+---+---+---+ +---+---+---+---+---+---+---+---+---+ |753| 64|759| 74|757|886|761| 78| 81| |535|586|479|340|481|342|475| 96| 93| +---+---+---+---+---+---+---+---+---+ +---+---+---+---+---+---+---+---+---+ ... ... +---+---+---+---+---+---+---+---+---+ +---+---+---+---+---+---+---+---+---+ |128|125|414| 49|130|123|410| 47|136| |206| 39|172|113|162| 37| 34|111| 30| +---+---+---+---+---+---+---+---+---+ +---+---+---+---+---+---+---+---+---+ |415| 50|129|124|413| 48|131|122|133| |159|114|161| 38|173|112| 31| 36| 33| +---+---+---+---+---+---+---+---+---+ ... +---+---+---+---+---+---+---+---+---+
Index | CV | Updated: Sat Oct 8 12:33:59 2022 |