pythonrecursionbacktracking

Question from recursive code - n queens problem


I was attempting a recursive solution to the n queens problem. Please take a look at this code:

def solve(n,chess_arr,forbidden_cells=[]):

  print("1.",n,chess_arr,forbidden_cells)

  if n>(n*n)-len(forbidden_cells):
    return False

  elif n==1:
    printchessboard(chess_arr)
    return True

  else:
    for i in range(len(chess_arr)):
      for j in range(len(chess_arr[i])):

        print("2. ",n,(i,j),(i,j) not in forbidden_cells,forbidden_cells)

        if (i,j) not in forbidden_cells:
          chess_arr[i][j]=["Q"]
          forbidden_row=i
          forbidden_col=j
          partial_forbidden_cells=create_partial_forbidden(n,chess_arr,forbidden_row,forbidden_col,forbidden_cells)
          forbidden_cells.extend(partial_forbidden_cells)

          if solve(n-1,chess_arr,forbidden_cells):
            return True
          else:
            chess_arr[i][j]=[]
            for partial in partial_forbidden_cells:
              forbidden_cells.remove(partial)
    return False


def create_partial_forbidden(n,chess_arr,forbidden_row,forbidden_col,forbidden_cells):
  partial_forbidden_cells=[]

  ######################################### This block takes care of rows and columns #################################################
  partial_forbidden_cells.extend([(forbidden_row+i,forbidden_col )\
                          for i in range(-n,n)\
                          if (forbidden_row+i>=0 and forbidden_row+i<n and\
                          (forbidden_row+i,forbidden_col) not in forbidden_cells )])
  partial_forbidden_cells.extend([(forbidden_row,forbidden_col+i )\
                          for i in range(-n,n)\
                          if (forbidden_col+i>=0 and\
                              forbidden_col+i<n and i!=0  and\
                          (forbidden_row+i,forbidden_col) not in forbidden_cells )])
  # i!=0 ensures that the place where the queen is located is not repeated again
  ######################################### This block takes  care of the diagonals ###################################################
  partial_forbidden_cells.extend([(forbidden_row+i,forbidden_col+i)\
                          for i in range(-n,n) \
                          if (forbidden_row+i>=0 and\
                              forbidden_row+i<n and forbidden_col+i>=0 and\
                              forbidden_col+i<n and i!=0  and\
                          (forbidden_row+i,forbidden_col) not in forbidden_cells )])

  partial_forbidden_cells.extend([(forbidden_row-i,forbidden_col+i) \
                          for i in range(-n,n) \
                          if (forbidden_row-i>=0 and\
                              forbidden_row-i<n and forbidden_col+i>=0 and\
                              forbidden_col+i<n and i!=0  and\
                          (forbidden_row+i,forbidden_col) not in forbidden_cells )])
  # i!=0 ensures that the place where the queen is located is not repeated again
  #####################################################################################################################################

  #print("forbidden cells are ", forbidden_cells)
  return partial_forbidden_cells

I am facing a conceptual doubt here. While the first print statement always returns a non empty forbidden_cells list on several occasions but the second print statement always returns an empty list. I am at a loss why this is so? Could someone please explain.

My expectation: Since I am passing the forbidden_list as a parameter, I expect it to get updated with each iteration and at each iteration, I would like to be dealing with the most recent and updated forbidden_list.

A typical output is as follows:

1. 3 [[['Q'], [], [], []], [[], [], [], []], [[], [], [], []], [[], [], [], []]] [(0, 0), (1, 0), (2, 0), (3, 0), (0, 1), (0, 2), (0, 3), (1, 1), (2, 2), (3, 3)]
2.  4 (0, 1) True []

Thanks for the help!


Solution

  • There are the following issues in your code:

    If you fix the above points it should work. A few more remarks:

    Fixed code

    Here is your code with the above points addressed, and with an example run for a standard chess board size (8):

    # Added parameter for more clues in the output
    def printchessboard(chess_arr, forbidden_cells):
        for i, row in enumerate(chess_arr):
          for j, cell in enumerate(row):
            print("X" if (i, j) in forbidden_cells and cell == "." else cell, end=" ")
          print()
        print()
    
    
    # Rename first parameter, remove mutable default value from last paramter
    def solve(queens, chess_arr, forbidden_cells):
      n = len(chess_arr)  # Make distinction between queens and n.
    
      # Corrected backtracking condition:
      if queens > n*n-len(forbidden_cells):
        return False
      # Corrected base case condition
      elif queens==0:
        return True
    
      else:
        for i in range(len(chess_arr)):
          for j in range(len(chess_arr[i])):
            if (i,j) not in forbidden_cells:
              chess_arr[i][j] = "Q"  # not ["Q"]
              forbidden_row = i
              forbidden_col = j
              # Drop the chess_arr argument:
              partial_forbidden_cells = create_partial_forbidden(n, 
                                 forbidden_row, forbidden_col, forbidden_cells)
              forbidden_cells.extend(partial_forbidden_cells)
              # Pass queens-1 as argument
              if solve(queens-1, chess_arr, forbidden_cells):
                return True
              else:
                chess_arr[i][j] = "." # Not list, but string
                for partial in partial_forbidden_cells:
                  forbidden_cells.remove(partial)
        return False
    
    # chess_arr parameter is not used
    def create_partial_forbidden(n, forbidden_row, forbidden_col, forbidden_cells):
        partial_forbidden_cells=[]
    
        partial_forbidden_cells.extend([
            (forbidden_row + i, forbidden_col)
            for i in range(-n, n)
                if  forbidden_row + i >= 0 and forbidden_row + i < n and
                    (forbidden_row + i, forbidden_col) not in forbidden_cells
        ])
    
        partial_forbidden_cells.extend([
            (forbidden_row,forbidden_col + i)
            for i in range(-n, n)
                if (forbidden_col + i >= 0 and forbidden_col + i < n and i!=0  and
                    # correction of tuple, here and in the next blocks
                    (forbidden_row,forbidden_col+i) not in forbidden_cells)
        ])
    
        partial_forbidden_cells.extend([
            (forbidden_row + i, forbidden_col + i)
            for i in range(-n, n)
                if (forbidden_row + i >=0 and forbidden_row + i < n and 
                    forbidden_col + i >=0 and forbidden_col + i < n and i!=0  and
                    (forbidden_row + i, forbidden_col + i) not in forbidden_cells)
        ])
      
        partial_forbidden_cells.extend([
            (forbidden_row - i,forbidden_col + i)
            for i in range(-n, n)
                if (forbidden_row - i >= 0 and forbidden_row - i < n and 
                    forbidden_col + i >= 0 and forbidden_col + i < n and i!=0  and
                    (forbidden_row - i, forbidden_col + i) not in forbidden_cells)
        ])
        return partial_forbidden_cells
    
    def main():
        n = 8
        chess_arr = [
            ['.'] * n
            for _ in range(n)
        ]
        solve(n, chess_arr, [])
        print("solution:")
        printchessboard(chess_arr, [])
    
    main()
    

    There is still room for improvement: