I've been working on analysing memory and timing profile of Knight's Tour in Python with blind depth-first search, and wrote a simple program to be able to generate solutions, or so I thought.
My task is to choose 5 initial starting points on the chessboard and find a solution for each one (with max 1mil steps to give up on trying to find a solution) where one starting point is always 0,0
My problem is that it takes a LONG time to take 100'000 steps and I need to speed it up. I'm however not sure how, and don't see what could be the issue and main slowing factor.
Additionally, I surpass even the extended recursion limit.. Is it okay to increase it further?.. The way I coded the method, I think it should be fine since I'm carrying over minimal data.
from random import *
from itertools import product # Easy random unique tuple generation
from pprint import pprint # To allow pretty printing for better analysis
import numpy as np # Will allow for arrays, where arithmetic is MUCH faster than in lists
import sys
import time
# TODO tests
class Euler:
def __init__(self, size_board: int, max_steps: int = None, random_seed: int = None):
self.max_steps = max_steps
if self.max_steps is None:
self.max_steps = 10e6
if random_seed is not None:
seed(random_seed) # Used for testing purposes to get same output every time.
self.size = size_board
self.max_depth = self.size ** 2
self.start_positions = sample(list(product(range(1, self.size), repeat=2)), k=4) # Generate random 2D positions
self.start_positions.insert(0, (0, 0)) # Add fixed [0, 0] position to compare 6x6 and 5x5 easily
print("Starting positions: ")
pprint(self.start_positions)
self.solutions = [] # Looking for first 5 solutions for each starting position
self.moves = [(1, 2), # Allowed jumps
(1, -2),
(2, 1),
(2, -1),
(-1, 2),
(-1, -2),
(-2, 1),
(-2, -1)]
for pos in self.start_positions:
self.steps_left = self.max_steps
self.board = np.array([[-1 for i in range(self.size)] for i in range(self.size)]) # Generate the chessboard
self.board[pos[0]][pos[1]] = 1 # Set first position as 1
self.blind_dfs(pos, 1)
for solution in self.solutions:
print("Solutions for starting position ", self.start_positions[0])
pprint(solution)
print()
self.start_positions.pop(0)
# Externally uses steps_left. The chessboard array is passed by reference, not copies. This is a procedure.
def blind_dfs(self, start, depth):
# If we're on a position labeled as the last possible position, we've found a solution
if depth == self.max_depth:
self.solutions.append(self.board.copy())
print("Found solution")
return -1 # Terminate recursion chain
for move in self.moves:
"""
We have to place this condition here, because this cycle could happen while there were steps
left, and if we put this outside, it would have no way of knowing to stop
the cycle even when out of steps, on older recursion depths
"""
if self.steps_left < 1:
self.solutions.append("Ran out of steps")
print("Solution not found")
return -1
step = tuple(np.add(start, move)) # Add the movement to the current position
# Check if we go outside the chessboard. If a number from the pair in the tuple is negative or > size, skip
if sum(1 for number in step if number < 0) == 0 and all(i < self.size for i in step):
if self.board[step] == -1:
self.board[step] = depth + 1 # Set the correct step order on the next position
self.steps_left -= 1
"""if self.steps_left % 100000 == 0: # Simple tracking
print(self.steps_left)"""
if self.blind_dfs(step, depth + 1) == -1:
return -1 # Annihilate recursion chain
self.board[step] = -1 # Going to try a new direction, so undo the step
else:
continue
sys.setrecursionlimit(2000) # Default is 1000. It is safe to modify it here, the stackframes are not big.
while (size := input("Enter size of board NxN: ")) != "":
try:
size = int(size)
except ValueError:
print("Please enter an integer.")
continue
steps = input("Enter max amount of steps (leave empty for 10e6 steps): ")
if steps == "":
steps = None
else:
try:
steps = int(steps)
except ValueError:
print("Please enter an integer.")
continue
Euler(size, steps, 4)