Oct 26th, 2014

Scala has a very nice lazy evaluation system, allowing you to defer a lot of evaluations to the last moment. This is a nice to have feature when you are dealing with 'almost-infinite' lists. There are some examples on the web but I wanted bigger trees to explore in order to see it working, so I went to sudoku.

Board basics#

Before going any further on the solving algorithm, we need to define some values and types

type Position = (Int, Int)              // the type to handle pairs of coordinates
val EmptyValue = '0'                    // the char representing the empty cells
val MaxValue = 9                        // the number of columns and rows

val allValues = "123456789".toList      // the list of all possible values of a cell
val indexes = (0 to 8).toList           // the list of column and row indexes

Now we can go with the Board class. This will be responsible for parsing the game and generate a list of the next possible 'movements'. First of all, let's look at the object initialization phase

class Board(val game: String) {
    val empty = game indexOf EmptyValue
    val emptyPosition = (empty % MaxValue, empty / MaxValue)
    val isDone = empty == -1

    ...
}

Here we see that Board has a constructor param called game. On the object instantiation, it calculates the first empty position in the board. It also checks if the board is completed (when there are no more empty cells, we can assume the game is done).

Encapsulating the base info in a string allows us to manipulate it easily. It also allows us to override the toString method so the board will be displayed in a more human friendly way.

override def toString: String = "\n" + (game sliding (MaxValue, MaxValue) mkString "\n")

Since our Board is based on a string definition of the game, we can add a function to create a new updated object with the same signature the String class exposes, so updating the Board will be as intuitive as updating the string that defines the object itself.

def updated(pos: Int, value: Char): Board = new Board(game updated (pos, value))

The Board class must include some accessors to the cells it contains, so we must implement some getters by row, column and box (here, I'm calling each 3x3 block a box).

def row(y: Int): List[Char] = indexes map (col => game(y * MaxValue + col))
def col(x: Int): List[Char] = indexes map (row => game(x + row * MaxValue))
def box(pos: Position): List[Char] = {
  def base(p: Int): Int = (p / 3) * 3

  val x0 = base(pos._1)
  val y0 = base(pos._2)
  val ys = (y0 until y0 + 3).toList
  (x0 until x0 + 3).toList.flatMap(x => ys.map(y => game(x + y * MaxValue)))
}

As you can see, in order to get the values of a row or a column we just map the indexes [0,8] with the corresponding values of the game. Getting the box values is more complex because the boundaries of the box must be calculated before extracting the values of the game.

Next candidates#

The easiest way to get a list of all possible values of a cell is to join all the values already placed in the same row, column and box. So we need a function to calculate the values a cell should not contain

def toAvoid(pos: Position): List[Char] = (col(pos._1) ++ row(pos._2) ++ box(pos)).distinct

and then, get the values not included in the toAvoid list

def candidates(pos: Position): List[Char] = allValues diff toAvoid(pos)

It is interesting to note that if there is not a single candidate, it will return an empty list.

The main objectives of the Board class was to encapsulate the game info and generate a list of the next possible movements. The first part is done. Let's finish the other one with the next function. Here we want to create a new Board for each candidate it found but we want to defer they evaluation until the last moment. So, now is when streams come to the rescue. A Stream defers the evaluation of the second param until it is required. By returning a Stream[Board] we avoid to instantiate every possible candidate until the code needs the next one to check if the game is finished or to ask for the next candidates.

def next: Stream[Board] = candidates(emptyPosition).toStream map (c => updated(empty, c))

When it returns an empty Stream, you are done or you have some cell(s) wrong. Anyway, that is the way you know the path is finished.

At this moment, we are done with our Board class. Hurray!

The solver#

Our Sudoku solver has to expose a board builder that instantiates our initial Board. Very straightforward task.

def build(game: String): Board = new Board(game)

Time to create an almost infinite list of possible boards. We want to explore the tree efficiently, so we will use a breadth-first traversal algorithm. First, we emit the head, then we add its subnodes to the end of the list of nodes to explore and move to the next node of the level. When all the nodes in a level are checked, we move to the next one by inspecting the children of the first node of the already completed level.

Note we need to accept a list of boards in order to make the function recursive. This function will get the head of the Stream and then it will create a new Stream with that item and the result of the recursive call. The Stream passed to that recursive call is just the concatenation of the tail of the original Stream and the candidates returned by the head. Once the tail and the next candidates are empty, the path is finished.

def from(path: Stream[Board]): Stream[Board] = path match {
    case h #:: t => h #:: from(t ++ h.next)
    case _ => Stream.empty
}

Due the way our Board class generates the candidate Stream, we can assume our list will contain just valid boards, mostly incomplete.

Now we have a builder and a candidate generator, we can put it together and generate a Stream of valid boards starting from the received game definition string.

def steps(game: String): Stream[Board] = from(Stream(build(game)))

The last function we must implement is the one that filters the Stream looking for a solved board. That is why we created the isDone val.

def solve(game: String): Board = steps(game).filter(_.isDone).head

And we are done! Our solver will just evaluate the minimun steps required to get the first solved board.

Further optimizations#

In order to avoid some repetitive calculations, it is possible to create a map with the positions a box contains out of the Board class and change the box function to get it filtering the game values with the precalculated map.

val boxCoordinates = (0 to 2).toList

def box(pos: Int): List[Int] = {
    def base(p: Int): Int = (p / 3) * 3

    val x0 = base(getX(pos))
    val y0 = base(getY(pos))
    val ys = (y0 until y0 + 3).toList
    (x0 until x0 + 3).toList.flatMap(x => ys.map(x + _ * 9))
}

val boxes = boxCoordinates flatMap (x => boxCoordinates map (x * 3 + _ * 3 * 9)) map box

class Board(val game: String) {
    ...
    def box(pos: Int): List[Char] = (boxes filter (_ contains pos)).head map game
    ...
}

The complete code is availbe as a gist;


Tags: