2

I'm trying to write a Python program which will solve the Knight's Tour problem using backtracking. For those unfamiliar: "The knight is placed on the first square of an empty chess board and, moving according to the rules of chess, must visit each square exactly once."

My code works mostly but not completely. It usually gets the 7th move and then returns an unfinished board. Why?

board = [
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0]]

def move(x, y, count, move_x, move_y):
    if count == len(board)**2:
        return True
    for i in range(8):
        if is_valid(x + int(move_x[i]), y + int(move_y[i]), board):
            if move(x + move_x[i], y + move_y[i], count + 1, move_x, move_y):
                board[x][y] = count
                return True
    return False
    

def print_board(bo):
    for i in range(len(bo)):
        for j in range(len(bo[0])):
            if j == (len(board[0]) - 1):
                print(bo[i][j])
            else:
                print(bo[i][j], end = " ")


def is_valid(x, y, board):
    if x >= 0 and y >= 0 and x < len(board) and y < len(board) and board[x][y] == 0:
        return True
    return False



def solve(bo):
    print_board(board)
    move_x = [1, 2, 2, 1, -1, -2, -2, -1]
    move_y = [2, 1, -1, -2, -2, -1, 1, 2]
    counter = 1
    x_loc = 0
    y_loc = 0

    if not(move(x_loc, y_loc, counter, move_x, move_y)):
        print("No solution")
    else:
        print("                 ")
        print_board(board)

solve(board)
    
smci
  • 32,567
  • 20
  • 113
  • 146
Lou
  • 103
  • 10
  • This is backtracking but isn't fully recursive (or OOP): `board` is a global, and `move()` overwrites the global `board`. Rather than declaring a class `Board` and an instance `board` with an internal state which you can save and restore. As such, this isn't able to generally ["solve" Knights Tour](https://en.wikipedia.org/wiki/Knight%27s_tour#Number_of_tours), it only finds the first solution. – smci Oct 22 '20 at 00:01
  • Also, the for-loop inside `move()` always returns the first legal move it finds (i.e. brute-force, it doesn't apply e.g. [Warnsdorff's heuristic: choose the square with the fewest legal onward moves](https://en.wikipedia.org/wiki/Knight%27s_tour#Finding_tours_with_computers), which would massively reduce your combinatorial explosion. Rather than constructing a hash of all fail-states, which will use insane amounts of memory. – smci Oct 22 '20 at 00:10

2 Answers2

4
  • The recursive call to move() should come after setting board[x][y] = count.
    • Moreover, if move() returns false, then you should have board[x][y] = 0
  • In fact, count does not need to be stored in each board cell. It just needs to be passed and incremented in each recursive call. You can use a boolean indicating whether a cell has been visited or not.
  • Final thoughts: There are 2^64 board states and many more paths to hit failure states repeatedly. As such, the failure states should be stored in a hash to avoid searching the failed paths repeatedly.
Tarik
  • 10,810
  • 2
  • 26
  • 40
  • Thanks for the help Tarik! I have implemented this and my program works! However, as I'm sure you're aware, it is a tad slow. Could you provide any insight as to how I should store failure states in hash? I'm relatively new to this and I'm not sure what this means... – Lou Aug 12 '20 at 07:29
  • 1
    Happy to hear you made it. You could store the failed board states as a Tuple containing the values of all the cells. A more efficient way to store that would be to generate a 64 bit unsigned integer out of to he individual True/False values found in each board cell. You then add the Tuple or Long Integer in a Set. Prior to any recursive call, check that the state you are in does not belong to the failed state Set. When you reach a dead end, store the failed state and then return. – Tarik Aug 12 '20 at 07:37
  • 1
    Note: Python does not have an unsigned type, so you have to map the 64 bit value spanning 0 to 2^64 to the range -2^61 to 2^61 - 1. – Tarik Aug 12 '20 at 07:43
  • Thanks for the info! I'll be honest, I don't completely understand your more efficient method (I'm quite new to programming). Your first idea sounds interesting though, I'll append every move to a list and if it's a dead end then I'll save it. Then maybe I can implement a function which checks if the failed path isn't being repeated. – Lou Aug 12 '20 at 08:51
  • 1
    An long integer is made up of 64 bits that you can access via bitwise operators. You have 64 cells in a board. You can represent the value of each cell by a single bit (1/0) in an integer. Example, if row 0, column 2 has a True and row 5 column 3 has True, you would end up with an integer that contains 64 zeros except for bit position 0 x 8 + 2 and bit position 5 x 8 + 3 that would contain a 1: 0000000000 0000000000 0000000000 0000001000 0000000000 0000000000 0000000000 0000000100 which when converted to decimal gives 8796093022212 – Tarik Aug 12 '20 at 09:07
  • I think maybe this is a little out of my wheelhouse at the moment. I'd have no idea how to do this. Thanks for the help though! – Lou Aug 12 '20 at 09:31
  • 1
    If you feel like tackling something like that in the future, you should break down the problem and implement functions to transform the board into an integer and back. Once you test that functionality separately and are confident it works, you can then integrate it into the solution. Not insisting you do it, but just wanted to remove that overwhelming feeling that it's too complex for you. – Tarik Aug 12 '20 at 11:53
  • That's not a bad idea! You make it sound very easy. If I get round to it I'll have to give you credit. Thanks for the help once again! – Lou Aug 12 '20 at 14:37
1
    def process(i,j,cnt,board,n,query):
    board[i][j]=cnt
    cnt+=1
    if cnt==n*n+1:
        for a in range(n):
            print(board[a])
        print()
        if query==0:
            return True
        else:
            True
    stack=[]
    
    if i+2<n and j-1>-1 and board[i+2][j-1]==-1:
        stack.append([i+2,j-1])
    if i+1<n and j-2>-1 and board[i+1][j-2]==-1:
        stack.append([i+1,j-2])
    if i-1>-1 and j-2>-1 and board[i-1][j-2]==-1:
        stack.append([i-1,j-2])
    if i-2>-1 and j-1>-1 and board[i-2][j-1]==-1:
        stack.append([i-2,j-1])
    if i-2>-1 and j+1<n and board[i-2][j+1]==-1:
        stack.append([i-2,j+1])
    if i-1>-1 and j+2<n and board[i-1][j+2]==-1:
        stack.append([i-1,j+2])
    if i+1<n and j+2<n and board[i+1][j+2]==-1:
        stack.append([i+1,j+2])
    if i+2<n and j+1<n and board[i+2][j+1]==-1:
        stack.append([i+2,j+1])

    while (len(stack))>0:
        curr=stack.pop()
        if query==0:
            if(process(curr[0],curr[1],cnt,board,n,query)):
                return True
            else :
                board[curr[0]][curr[1]]=-1
        else:
            process(curr[0],curr[1],cnt,board,n,query)
            board[curr[0]][curr[1]]=-1
    return


########     Driver Code     ########



    query=input("If you want program to output all possible ways of KNIGHT'S TOUR, say Yes or else say No :- ")
    if query[0]=='y' or query[0]=='Y':
        query=input("This process may be too slow for N greater than 5, it will work fast if N is less than 6. Are u sure u want to go for it? (Yes/No) :- ")
    if query[0]=='y' or query[0]=='Y':
        query=1
    else :
        query=0
    n=int(input("Enter the value of N :- "))
    if(n<5):
        print("Not possible for N less than 5")
    else:
        board= [[-1]*n for i in range(n)]
        cnt=1
        process(0,0,cnt,board,n,query)

    

########     Some precomputed outputs     ########


# N = 5
#
# [1, 12, 3, 18, 21]
# [4, 17, 20, 13, 8]
# [11, 2, 7, 22, 19]
# [16, 5, 24, 9, 14]
# [25, 10, 15, 6, 23]


# N = 6
#
# [1, 20, 3, 18, 5, 22]
# [36, 11, 28, 21, 30, 17]
# [27, 2, 19, 4, 23, 6]
# [12, 35, 10, 29, 16, 31]
# [9, 26, 33, 14, 7, 24]
# [34, 13, 8, 25, 32, 15]


# N = 7
#
# [1, 14, 3, 38, 5, 34, 7]
# [12, 39, 10, 33, 8, 37, 26]
# [15, 2, 13, 4, 25, 6, 35]
# [40, 11, 32, 9, 36, 27, 44]
# [19, 16, 21, 24, 45, 48, 29]
# [22, 41, 18, 31, 28, 43, 46]
# [17, 20, 23, 42, 47, 30, 49]


# N = 8
#
# [1, 60, 39, 34, 31, 18, 9, 64]        
# [38, 35, 32, 61, 10, 63, 30, 17]      
# [59, 2, 37, 40, 33, 28, 19, 8]        
# [36, 49, 42, 27, 62, 11, 16, 29]      
# [43, 58, 3, 50, 41, 24, 7, 20]        
# [48, 51, 46, 55, 26, 21, 12, 15]      
# [57, 44, 53, 4, 23, 14, 25, 6]        
# [52, 47, 56, 45, 54, 5, 22, 13]
  • Here is another code for Knight's Tour. It too works on concepts of Backtracking. – Manish Salunkhe Nov 07 '21 at 04:44
  • 1
    Please [edit] your answer to fix your indentation. If you could add more of an explanation so that others can understand your solution, that would also be helpful. – David Buck Nov 07 '21 at 06:50
  • 1
    Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Nov 07 '21 at 07:06