ThreadedNQueens.py

From dftwiki3
Jump to: navigation, search

--D. Thiebaut 16:06, 24 January 2010 (UTC)


#! /usr/bin/python
# D. Thiebaut
# Usage:
#   nqueens.py N
# where N is the number of queens wanted, i.e. the dimension
# of the board.
#
# to kill a program that doesn't want to stop:
# kill `ps | grep threaded | grep -v idle | grep -v grep | cut -d' ' -f 1`


import sys
import time
import threading


QUEEN = -10    # marker for a queen in 2D integer array
EMPTY = 0      # marker for empty cell (no queen, no coverage by a queen)


class timePerfClass:
  """A class to keep track of processor and user time.
  use by first calling start(), then stop(), then
  printElapsed()"""

  def __init__(self):
      self.clockStart = 0   # beginning processor time
      self.clockEnd   = 0   # ending processor time
      self.timeStart  = 0   # beginning user time
      self.timeEnd    = 0   # ending user time

  def start( self ):
      self.clockStart = time.clock()
      self.timeStart  = time.time()

  def stop( self ):
      self.clockEnd   = time.clock()
      self.timeEnd    = time.time()

  def printElapsed( self ):
      print "processor time:    ", (self.clockEnd-self.clockStart), "sec"
      print "user time     :    ", (self.timeEnd-self.timeStart), "sec"


class queenThread( threading.Thread ):
  """
  a thread for searching for a solution to the N-Queens problem
  starting from a given location on the top row.  The location is
  stored in self.col.  The thread then tries to place N-1 queens
  on the remaining N-1 rows.
  run() is the main core of the thread
    self.timeToStop   set to True by main to force the thread to quit
  self.ok           True if thread found a solution
  self.N            the dimension of the board
  self.board        the NxN board used by thread to find solution
  self.doneEvent    an Event shared by all theads and main. cleared
                    by main() which bock-waits on it until a thread
                    finds a solution and sets the event, which awakens
                    main().

  """
  def __init__( self, N, col, doneEvent ):
      """constructor.  Initializes parent class and set all local members"""
      threading.Thread.__init__( self )
      self.col        = col
      self.N          = N
      self.doneEvent  = doneEvent
      self.board      = self.makeBoard( N )
      self.ok         = False
      self.timeToStop = False

  def run( self ):
      """will be called by start(), automatically.  The workhorse member function
      that does all the work"""
      self.board[ 0 ][ self.col ] = QUEEN
      self.markLowerBoard( self.board, 0, self.col, 1 )                    
      self.ok = self.tryRow( self.board, 1, self.N )

      #--- tell main program if we've found a solution ---
      if self.ok:
         self.doneEvent.set()

  def stop( self ):
      """call this member function to tell the thread to stop running.  Another thread
      has found a solution."""
      self.timeToStop = True

  def getStatus( self ):
      """returns whether the thread was successful (ok=true) or not finding 
      a solution """
      return self.ok

  def getBoard( self ):
      """return the current board with the queens positions"""
      return self.board

  def makeBoard( self, N ):
     """create a 2-D array of ints with dimension N
     Returns the 2D array"""
     board = []
     for i in range( N ):
         board.append( [] )
         for j in range( N ):
             board[i].append( EMPTY )
     return board

  def markLowerBoard( self, board, row, col, offset ):
     """Once a queen is positioned at location (row,col),
     all the cells on a lower diagonal and lower vertical
     of this queen must be marqued as unavailable so that
     another queen cannot be put in this place"""

     N = len( board )
     diagleft = col-1
     diagright = col+1
     # mark all lower rows on diagonals and vertical
     for r in range( row+1, N ):
         if diagleft >=0:  board[r][diagleft] += offset
         if diagright <N:  board[r][diagright] += offset
         board[r][col] += offset
         diagleft  -= 1
         diagright += 1

  def tryRow( self, board, row,  N ):
     """ put a queen on give row, and recursively try all queens
     on successive rows"""
     if row >= N:
        return True #we found a solution!

     for col in range( N ):
         if self.timeToStop:
             return False
         if board[row][col] == EMPTY:
             # put a queen here
             board[ row ][ col ] = QUEEN
             self.markLowerBoard( board, row, col, +1 )
             ok = self.tryRow( board, row+1, N )
             if not ok:
                 # backtrack
                 board[ row ][ col ] = EMPTY
                 self.markLowerBoard( board, row, col, -1 )
             else:
                 return True
     return False                       

def goHome():
      """on terminals supporting ANSI chars, puts cursor top left of screen"""
      sys.stderr.write( "\x1b[0;0H" )


def displaySolution( board ):
  """displays the series of column indexes where the Queens reside"""
  list = []
  for i in range( len( board ) ):
      for j in range( len( board ) ):
          if board[ i ][ j ]==QUEEN:
              list.append( str( j ) )
  print "Solution = ", ','.join( list )

def displayBoard( board, home=False ):
  """display the 2D array, showing empty cells as .
  and queens as Q"""
  if home: goHome()
  for i in range( len( board ) ):
      for j in range( len( board ) ):
          if board[i][j]==QUEEN:
             print 'Q',
          else:
             print '.',
      print
  displaySolution( board )


#------------------------------------------------------------------------
def main():
  """
  Finds the solution to the N-Queens by launching N threads, each
  one assuming the first queen is located on the Nth column of the
  first row of the board.
  """
  if len( sys.argv ) < 2:
      print "Syntax: threadedNQueens.py N"
      print "        where N is the # of queens"
      return

  #--- start measuring execution time ---
  perf = timePerfClass()
  perf.start()
    #--- get dimension of board ---
  N = int( sys.argv[1] )

  #--- create an event that will enable a thread to signal main() ----
  doneEvent = threading.Event()
  doneEvent.clear()

  #--- create N threads and keep them in a list ---
  threadList = []
  for col in range( N ):
      newThread = queenThread( N, col, doneEvent ) # new thread
      threadList.append( newThread )               # add to list
      newThread.start()                            # start it

  print "all threads started!"

  #--- wait for first successful thread to issue a notify on cond ---
  print "blocking and waiting for a thread to succeed..."
  doneEvent.wait()
  print "Solution found!"

  #--- stop timing and display execution times ---
  perf.stop()
  perf.printElapsed()

  #--- find (a) thread with a solution and display it---
  for threadQ in threadList:
      if threadQ.getStatus() == True:
         displayBoard( threadQ.getBoard() )
         break

  #--- tell all threads to stop ---
  print "notifying threads to stop"
  for threadQ in threadList:
      threadQ.stop()

  #---waiting for threads to finish ---
  while len( threading.enumerate() ) > 1: # 1 because main is still running
     time.sleep( 0.5 )


if __name__=="__main__":
  main()