N-Queens/Sudoku Assignment

From CSE231 Wiki
Jump to navigation Jump to search

Motivation

Not everything in the world should be divided and conquered. Backtracking is a powerful technique which can be readily parallelized. We will gain experience with backtracking by solving the N-Queens problem and Sudoku in parallel.

N-Queens in particular can be used to explain the call stack as the chessboard *IS* the call stack. Whoa.

In this assignment, you will implement solutions to both the n-queens and Sudoku problems.

N-Queens

Example solution of N-Queens when n equals 8

Background

The n-queens problem is a fundamental coding puzzle which asks: how can N queens be placed on an NxN chessboard so that they cannot attack each other? In chess, a queen can move horizontally, vertically, and diagonally across the board. Thus, to solve the n-queens problem, we must effectively figure out how to place the queens in such a way that no two of them occupy the same row, column, or diagonal. We will be building a method that finds the total number of solutions for n-queens for any given n.


Roadmap to Victory

  1. DefaultImmutableQueens
  2. QueenLocationsUtils
  3. SequentialNQueens
  4. ParallelNQueens

Code To Implement

Before coding anything, take a look at the javadocs to see what everything does and what you will need to implement.

Sequential Solution

	public static int countSolutions(MutableQueenLocations queenLocations) {
		MutableInt count = new MutableInt(0);
		placeQueenInRow(count, queenLocations, 0);
		return count.intValue();
	}
class: SequentialNQueens.java Java.png
methods: placeQueenInRow
package: nqueens.lab
source folder: student/src/main/java

method: private static void placeQueenInRow(MutableInt count, MutableQueenLocations queenLocations, int row) Sequential.svg (sequential implementation only)

Parallel Solution

Board State

Investigate DefaultMutableQueenLocations and AbstractQueenLocations for clues on how to implement DefaultImmutableQueenLocations.

class: DefaultImmutableQueenLocations.java Java.png
methods: createNext
getColumnOfQueenInRow
getRowCount
getBoardSize
package: nqueens.lab
source folder: student/src/main/java

method: public DefaultImmutableQueenLocations createNext(int column) Sequential.svg (sequential implementation only)

There are two constructors for this class. A public one which creates a fresh new board state with no queens yet placed. and a private one which creates a new board with the state of a given board which is further constrained by a new queen in the next row. You need to create a new instance using one of these two constructors. Which one is it?

method: public int getColumnOfQueenInRow(int row) Sequential.svg (sequential implementation only)

method: public int getRowCount() Sequential.svg (sequential implementation only)

method: public int getBoardSize() Sequential.svg (sequential implementation only)

The three methods above can all be done in just one line. Don't make things too complicated! Note that we will refer to the standard 8x8 chessboard's size as 8 and not 64.

method: public boolean isNextRowThreatFree(int column) Sequential.svg (sequential implementation only)

Do not feel compelled to build this method from scratch. Investigate your super class for a utility method that will be helpful.

Board Utils

This class will provide methods that will speed up our implementation of the parallel solution in the final step.

class: QueenLocationsUtils.java Java.png
methods: getCandidateColumns
getCandidateColumnsForNextRow
package: nqueens.lab
source folder: student/src/main/java

method: public static Collection<Integer> getCandidateColumns(QueenLocations queenLocations, int row) Sequential.svg (sequential implementation only)

This method should find all the columns in the given row that a queen could be placed, and return them in a single collection (ex. LinkedList). Don't forget to utilize some of the methods you just completed in DefaultImmutableQueensLocation

method: public static Collection<Integer> getCandidateColumnsForNextRow(ImmutableQueenLocations queenLocations) Sequential.svg (sequential implementation only)

Nice and simple, don't overthink it! Just use methods you've already written to do all the work.

ParallelNQueens

Searching for solutions like n-queens can be done in parallel without the need to finish at each level. As such, forasync is preferable to forall. However:

Attention niels epting.svg Warning:Ensure that you complete all of your tasks by enclosing them all in a finish.
class: ParallelNQueens.java Java.png
methods: placeQueenInRow
countSolutions
package: nqueens.lab
source folder: student/src/main/java

method: public static int countSolutions(ImmutableQueenLocations queenLocations) Parallel.svg (parallel implementation required)

Attention niels epting.svg Warning:FinishAccumulators must be registered with their finish statement

Instead of using a MutableInt in order to count the number of solutions we have found, we want to use a Finish Accumulator. Refer to the syntax page in order to see the syntax for properly setting up the accumulator.

method: private static void placeQueenInRow(FinishAccumulator<Integer> acc, ImmutableQueenLocations queenLocations) Parallel.svg (parallel implementation required)

Make sure you look at the Sequential N-Queens solution if you need ideas on how to approach the algorithm.

Tips

  • As ImmutableQueenLocations is immutable, you will need to create a new instance of the object whenever you move on from one row to the next. This is where createNext comes in, along with the private constructor of this class.
  • The isNextRowThreatFree method can easily be completed with a method in AbstractQueenLocations. Refer to that for help.
  • The sequential solution uses MutableQueenLocations while the parallel solution uses your implementation of ImmutableQueenLocations. Be careful to use the correct QueenLocations implementation.
  • As the name suggests, placeQueenInRow will go through the columns of the given row to check if a queen can fit in that location. If it can, it will set that value in MutableQueenLocations. If the examined row is that last row of the board, you have found one valid solution to the n-queens problem. Update the correct parameter accordingly. Otherwise, recurse and keep going until you reach the last row.
  • For the parallel implementation of placeQueenInRow, we are using the "one-finish" pattern. Do not call finish in the recursive method.
  • The syntax for instantiating a FinishAccumulator of Integer is: FinishAccumulator<Integer> acc = newIntegerFinishAccumulator(NumberReductionOperator.SUM);
  • The syntax for using a FinishAccumulator is: finish(register(acc), () -> { //body });

Sudoku

Background

BasicSudoku.PNG

We will be using a similar algorithm to solve a Sudoku puzzle. For those not familiar, a Sudoku puzzle is composed of a 9-by-9 grid of squares. This grid is also divided into 9 large boxes, each of which is a 3-by-3 of the smaller squares. In a completed puzzle, each of the smaller squares contains a single number from 1 to 9 (inclusive). However, if a square contains a given number, that same number cannot be anywhere else in the same row, column, or box. Thus, for Sudoku, we are given an incomplete board and must fill in the remaining squares while meeting these requirements.

Sudoku is another problem well solved by backtracking. Check the understanding you gained of backtracking with N-Queens by challenging yourself to solve Sudoku's solver without assistance. The game of Sudoku is bit more complex though than N-Queens, and there are more strategies we can do than just backtracking in order to speed up our solution. To make this assignment more compelling, you will also do search orderings and constraint propagation.

Read Peter Norvig's Essay before you begin coding. It will cover everything related to the Sudoku problem itself and how one can design a solution for it.

Roadmap to Victory

There isn't one easiest path through the required files. Some classes utilize methods written in other files, so some students may take a the path such that they will only call methods that are provided or have already implemented. For other students, it might be conceptually easier to start with ParallelSudoku.java, since this class closest resembles the work you just did in n-queens. Below is one path that we recommend. In this path, you will have to call the constraint propagator in the Immutable Puzzle before completing the actual propagator, but aside from that most methods build on top of one another. In summary, you can work on these classes in whatever order makes the most sense for you personally.

  1. DefaultImmutableSudokuPuzzle
  2. RowMajorSquareSearchAlgorithm
  3. FewestOptionsFirstSquareSearchAlgorithm
  4. ParallelSudoku
  5. DefaultConstraintPropagator
  6. (Optional Challenge) Add Unit Constraint Propagation to DefaultConstraintPropagator

Code To Investigate

enum Square

Collection<Square> getPeers()
valueOf(row, column)
all enums have a values() method

class Units

public static Iterable<Collection<Square>> allUnits()

interface ConstraintPropagator extends AbstractSet<Integer> implements SortedSet<Integer>

class OptionSet

public static OptionSet createAllOptions()
public static OptionSet createSingleOption(int option)

class SquareToOptionSetMapUtils

public static Map<Square, SortedSet<Integer>> deepCopyOf(Map<Square, SortedSet<Integer>> other)

Code To Implement

Puzzle

As the name suggests, ImmutableSudokuPuzzle is immutable, and you will need to create a new instance of the object whenever you move on from one square to the next. This is analogous to the work you did for #NQueens.

class: DefaultImmutableSudokuPuzzle.java Java.png
methods: constructors
createNext
getValue
getOptions
package: sudoku.lab
source folder: student/src/main/java

method: public DefaultImmutableSudokuPuzzle(ConstraintPropagator constraintPropagator, String givens) Sequential.svg (sequential implementation only)

This constructor creates a puzzle constrained to an initial set of givens. You can think of the givens as the original values provided by the newspaper or airline magazine or puzzle book or whatever.

You will leverage the constraintPropagator to build your map of option sets.

Circle-information.svg Tip: To quickly make a map containing each possible Square, use new EnumMap<>(Square.class)

method: private DefaultImmutableSudokuPuzzle(DefaultImmutableSudokuPuzzle other, Square square, int value) Sequential.svg (sequential implementation only)

This constructor takes a given previous puzzle and a square value to create a new further constrained puzzle.

method: public ImmutableSudokuPuzzle createNext(Square square, int value) Sequential.svg (sequential implementation only)

This method should create a new puzzle instance using one of the constructors. Which one is it?

method: public int getValue(Square square) Sequential.svg (sequential implementation only)

Based on the state of the board, return the value of a given square if it is known. Otherwise, return 0.

How do we determine if a value for a given square is "known"?

method: public SortedSet<Integer> getOptions(Square square) Sequential.svg (sequential implementation only)

Based on the state of the board, return the candidate values for a given square.

Search Order

Simply by changing the search order, a great reduction of work can be achieved. The class names sadly give away the approaches.

Ask yourself:

  • Which algorithm will perform better and why?
  • What properties make a square "filled"?
class: RowMajorSquareSearchAlgorithm.java Java.png
methods: selectNextUnfilledSquare
package: sudoku.lab
source folder: student/src/main/java

method: public Square selectNextUnfilledSquare(SudokuPuzzle puzzle) Sequential.svg (sequential implementation only)

Simply run through the Square.values() which will iterate through squares going down the row (A1, A2, A3, ...). Make sure not to return squares that have already been filled.

class: FewestOptionsFirstSquareSearchAlgorithm.java Java.png
methods: selectNextUnfilledSquare
package: sudoku.lab
source folder: student/src/main/java

method: public Square selectNextUnfilledSquare(SudokuPuzzle puzzle) Sequential.svg (sequential implementation only)

Go through every square by calling Square.values(), find a square that is (1) not already filled, and (2) has the minimal number of possible options among all the squares, and return that square.

Solver

This part of the assignment is also similar to its n-queens counterpart. Searching for solutions like sudoku can be done in parallel without the need to finish at each level. As such, forasync is preferable to forall. However:

Attention niels epting.svg Warning:Ensure that you complete all of your tasks by enclosing them all in a finish.
class: ParallelSudoku.java Java.png
methods: solve
solveKernel
package: sudoku.lab
source folder: student/src/main/java

method: public static ImmutableSudokuPuzzle solve(ImmutableSudokuPuzzle puzzle, SquareSearchAlgorithm squareSearchAlgorithm) Parallel.svg (parallel implementation required)

method: private static void solveKernel(MutableObject<ImmutableSudokuPuzzle> solution, ImmutableSudokuPuzzle puzzle, SquareSearchAlgorithm squareSearchAlgorithm) Parallel.svg (parallel implementation required)

Tips

  • Make sure your solver uses the generic SquareSearchAlgorithm so that it works with both versions of the search ordering.
  • Just like n-queens, your solveKernel is the recursive method that will be called in the solve method. Again, watch where you put your finish.
  • In your solveKernel method, you should use the given search algorithm (row major or fewest options first) to select which square you will fill. After selecting a square, you should recursively call the kernel to check every viable option in the context of the puzzle. If there are no more unfilled squares, you should set the value of the solution to the finished puzzle and exit out of the recursion.

Constraint Propagator

class: DefaultConstraintPropagator.java Java.png
methods: createOptionSetsFromGivens
createNextOptionSets
assign
eliminate
package: sudoku.lab
source folder: student/src/main/java

You will need to implement two public methods to satisfy the ConstraintPropagator interface: createOptionSetsFromGivens and createNextOptionSets. Each of these two methods will be invoked from a different constructor in the #DefaultImmutableSudokuPuzzle class. It should be relatively obvious which one goes with which based on the parameters.

Attention niels epting.svg Warning: Resist the temptation to perform the constraint propagation in these public methods. Get things set up and then get your private method(s) to work for you.

createOptionSetsFromGivens

method: public Map<Square, SortedSet<Integer>> createOptionSetsFromGivens(String givens) Sequential.svg (sequential implementation only)

This method will be invoked when you have loaded a new puzzle. Whether it is from the newspaper, or your airline magazine, or Dr. Arto Inkala there is a set of givens the puzzle creator has provided. That string of givens is parsed into a double array of integers that aligns with the Sudoku board (values[0][0] is the top left corner of the board). If a specific spot of the board wasn't given in the input, it will be put into values[][] as 0.

A good approach here is to start of by initializing all of the squares to all the options (1 through 9) whether or not the square has a given value.

Once that is complete go through the givens and get your private method(s) to work for you. Note: Square.valueOf(row, column) should be useful here.

createNextOptionSets

method: public Map<Square, SortedSet<Integer>> createNextOptionSets(Map<Square, SortedSet<Integer>> otherOptionSets, Square square, int value) Sequential.svg (sequential implementation only)

This method should be invoked when you are searching for the solution in your solver. Much like when you need to create a new copy of the board every time you make a decision in the n-queens search so too will you need to create a new copy of your sudoku board every time you make a decision in your backtracking search.

Make sure to use the SquareToOptionSetMapUtils.deepCopyOf method and refrain from mutating the incoming parameter otherOptionSets.

As warned above, do not actually do any of the constraint propagation in this method. Simply get the copy of the options set, then send it through your private assign and eliminate to update the copy to be correct before returning it.

Circle-information.svg Tip:If you have two Lists, a and b, and you set a = b, then any changes you make to a will also be made to b. Both variables reference the same objects; they are not copies.

Assign and Eliminate

Peter Norvig's Essay is very helpful here. We have adopted his terms, but challenge yourself to complete this section without simply translating his pseudocode.

To simplify things a bit, we have elected to not short circuit when a 0 option square is found (relying on the search ordering to take care of that for us).

It is up to you how to utilize these two methods. The goal is to update the map that is passed in to reflect the "solving" of a square. For example, if assign is called on square A1 for the value 1, the map that is returned should link the square A1 to a list containing just the value 1 ([1]). Furthermore, any square that is a peer of A1 (ex. B1), should not have the value 1 in its set in the map.

But in solving Sudoku, there are some strategies that we can rely on to more efficiently complete the puzzle. The first one, and required in order to finish this lab, is Peer Elimination Constraint Propagation (rule 1 in the Norvig essay). The rule is as follows: "If a square has only one possible value, then eliminate that value from the square's peers."

Let's very slowly walk through a example of this propagation in detail to make it more clear what we are looking for. Start by looking at this nearly finished board:

Board Info
PeerConstraint1.PNG We can see that the red square (D1) has only one available option. At this point, let's say we call assign on the red square D1 for the value 1.
PeerConstraint3.PNG As part of the assignment process, all the peers of red square D1 have the value 1 removed from their possibilities. In this example, yellow square, A1, is directly affected! After eliminating the alternative option, there is only one possible value yellow square A1 could be. This is where our constraint propagator needs to take the next step and treat yellow square A1 as assigned.
PeerConstraint4.PNG This step right here (going from a small 7 to a big 7) is not actually something our program will do. In our implementation, having only one option in a square's set is equivalent to that square being "assigned" to that value. Because of this, you actually do not need to call assign again for yellow square A1.
PeerConstraint5.PNG Now that yellow square A1 is known, the final step is to go through all of A1's peers (including green square C3) and remove the value 7. Note that now there is only one possible option left for green square C3. Your code should keep going, looking at all the peers of C3 and eliminating the value 1, and so on. Some puzzles can be completely solved using only Peer Elimination constraint propagation!


method: private void assign(Map<Square, SortedSet<Integer>> resultOptionSets, Square square, int value) Sequential.svg (sequential implementation only)

Attention niels epting.svg Warning:If you are hitting a ConcurrentModificationException here, make sure to make a copy of the optionsSet you are iterating over. Use your local utility copyOf method.
	SortedSet<Integer> copy = copyOf(optionSet);
	for (int otherValue : copy) {
		...

method: private void eliminate(Map<Square, SortedSet<Integer>> resultOptionSets, Square square, int value) Sequential.svg (sequential implementation only)

(Optional) Unit Constraint Propagation

The second rule in Peter Norvig's Essay is Unit Constraint Propagation: "If a unit has only one possible place for a value, then put the value there". This propagation rule is it bit more challenging to put into code, but doing so can make even the hardest of Sudoku puzzles fall nice and quickly. To complete this bonus part of the lab, simply update your assign and eliminate methods to do unit constraint propagation (don't remove the old propagation rule!). After assigning a square, you'll have to look at the new options set to see if any unit (a single row, column, or box) has only one unassigned square that is a possibility for any value. We encourage you to think of a solution on your own! Below is a example of unit constraint propagation.

Board Info
UnitConstraint1.PNG We can see that the red square (E1) has only one available option. At this point, let's say we call assign on the red square E1 for the value 6.
UnitConstraint2.PNG Due to the assignment of red square E1, the value of 6 is eliminated from its peer, yellow square A1.
UnitConstraint3.PNG Our Peer Elimination constraint propagation will treat yellow square A1 as if it has been assigned. Unfortunately, eliminating the value 9 from the peers doesn't open up any new opportunities for solving squares. This is where unit constraint propagation kicks in. Of the three unassigned squares in the top left box unit, 6 is only a valid option in the green square, C3. Even though green square C3 has multiple possible options remaining, we know that it must be 6, and should assign it as such
UnitConstraint4.PNG Once green square C3 has been assigned (all other options are removed), we should make sure that all of C3's peers have 6 removed from their options. And keep an eye out for any new squares that we now know due to the two propagation rules! With both peer elimination and unit constraint propagation, we can solve a large quantity of Sudoku puzzles without needing any backtracking recursion

Testing Your Solution

Visualization

N-Queens

class: NQueensVizApp.java VIZ
package: nqueens.viz.solution
source folder: student/src//java

NQueensViz.png

Sudoku

class: SudokuSolutionApp.java VIZ
package: sudoku.viz.solution
source folder: student/src//java

SudokuViz.png

Correctness

There is a top-level test suite comprised of sub test suites which can be invoked separately when you want to focus on one part of the assignment.

top level

class: BacktrackTestSuite.java Junit.png
package: backtrack.lab
source folder: testing/src/test/java

sub

class: NQueensTestSuite.java Junit.png
package: nqueens.lab
source folder: testing/src/test/java
class: SudokuTestSuite.java Junit.png
package: sudoku.lab
source folder: testing/src/test/java

(Optional) Challenge Unit Constraint Propagation

class: ChallengeSudokuTestSuite.java Junit.png
package: sudoku.challenge
source folder: testing/src/test/java

Rubric

As always, please make sure to cite your work appropriately.

Total points: 100

N-Queens subtotal: 35

  • Correct DefaultImmutableQueenLocations (5)
  • Correct SequentialNQueens (10)
  • Correct ParallelNQueens (10)
  • Parallel ParallelNQueens (10)

Sudoku subtotal: 55

  • Correct DefaultImmutableSudokuPuzzle (5)
  • Correct ContraintPropagator (15)
  • Correct RowMajorSquareSearchAlgorithm (5)
  • Correct FewestOtionsFirstSquareSearchAlgorithm (10)
  • Correct ParallelSudoku (10)
  • Parallel ParallelSudoku (10)

Whole project:

  • Clarity and efficiency (10)