0

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)
Jack Avante
  • 1,405
  • 1
  • 15
  • 32
  • 1
    Two points: This is better suited to https://codereview.stackexchange.com/ and as a general rule I wouldn't recommend messing with the recursion limit in python -- use a stack instead (I didn't read your code, this is just generic advice) – Kenny Ostrom Mar 17 '20 at 15:30
  • Thanks, I didn't know codereview.stackexchange.com existed. Should I delete this post and put it there instead? – Jack Avante Mar 17 '20 at 15:31
  • Okay, I admit, I did set recursion limit 2000 to accomodate a DP solution where I knew the input size. ONCE. The only downside is performance, until you actually run out of memory/stack space (which happens sooner than you'd think). Function calls are very expensive (compared to other languages) in order to support python's exception handling. Huge strength/weakness tradeoff. – Kenny Ostrom Mar 17 '20 at 15:32
  • 1
    I don't think this is fit for CR yet. See [this](https://codereview.stackexchange.com/help/on-topic). – ggorlen Mar 17 '20 at 15:32
  • My comment is based on the assumption that it's working and correct, for smaller input sizes. That's how I read your question. – Kenny Ostrom Mar 17 '20 at 15:33
  • I don't think there is enough context here. Why do you need to take 100k steps on an 8x8 board? Why is this blowing the stack? Having to change the recursion limit is 99% of the time a design flaw. What's a "blind" DFS exactly? What is the point of the program/what problem is this supposed to solve? Are you trying to compare the time it takes to solve the Knight's Tour for different start squares? – ggorlen Mar 17 '20 at 15:35
  • Blind DFS means I am blindly looking for solution using depth first search by testing all possible moves, without any heuristic to improve speed of finding solutions. The 10e6 limit of steps is merely a limit after which we give up looking for a solution from a given starting square. Why we need to look for a solution on a 5x5 board from 5 various positions?.. Cause that's what they want me to do for the assignment. I don't know. I am not looking to compare anything specific. All I've been told to do is make observations of any kind. – Jack Avante Mar 17 '20 at 15:39
  • Ouch, brute force on a chess board? Not a chance. Okay keep it here. – Kenny Ostrom Mar 17 '20 at 15:40
  • What's the smallest test case that works correctly and finds a solution? – Kenny Ostrom Mar 17 '20 at 15:56
  • Searching from any corner seems to find a solution within approximately 50k steps. I'm more worried about the time it takes to take one step though. Something in the algo is very slow. – Jack Avante Mar 17 '20 at 16:03
  • I was expecting it to find a solution for 5x5, which I think should have a solution according to my reading of https://en.wikipedia.org/wiki/Knight's_tour But see their comments on brute force – Kenny Ostrom Mar 17 '20 at 16:05
  • I got solutions for 5x5 by raising the steps, which are total steps, not depth. You keep track of the depth, and it never exceeds size squared. This should never get anywhere close to the system recursion limit on function calls. Why do you think it needs sys.setrecursionlimit? I think you're just getting blown out of the water by normal math, not by stack memory. – Kenny Ostrom Mar 17 '20 at 16:26
  • Well all I know is that I gave it 10e6 steps limit (10,000,000), where one step means a valid jump of the Knight on the chessboard. Depth is my variable (albeit badly named) that represents how many board squares are already covered. As in if depth = 12 then I have made 12 valid jumps, as in the chessboard is covered in numbers 1 to 12 with 13 remaining for size 5x5 – Jack Avante Mar 17 '20 at 17:27

0 Answers0