Last time, I mentioned that I tried to write sudoku solver in haskell and it was using too much memory and time. So I tried to solve it again and this time I was able to do it, I guess I just needed more motivation.

We will be using this file format as input to our suodku program:

92634.7.1
.5..264.9
.7.8.1...
...9..2.7
342.....5
1.....8..
6854...12
..4..29..
.1.538.7.

and it will be represented as an array of numbers and . will be represented as 0, so for our repl, the sudoku will be [[Int]]

[[9, 2, 6, 3, 4, 0, 7, 0, 1],
 [0, 5, 0, 0, 2, 6, 4, 0, 9],
 [0, 7, 0, 8, 0, 1, 0, 0, 0],
 [0, 0, 0, 9, 0, 0, 2, 0, 7],
 [3, 4, 2, 0, 0, 0, 0, 0, 5],
 [1, 0, 0, 0, 0, 0, 8, 0, 0],
 [6, 8, 5, 4, 0, 0, 0, 1, 2],
 [0, 0, 4, 0, 0, 2, 9, 0, 0],
 [0, 1, 0, 5, 3, 8, 0, 7, 0]]

above sudoku only have one solution, but there could be sudokus with multiple solutions also, like:

[[2, 9, 5, 7, 4, 3, 8, 6, 1],
 [4, 3, 1, 8, 6, 5, 9, 0, 0],  -- 2 7
 [8, 7, 6, 1, 9, 2, 5, 4, 3],
 [3, 8, 7, 4, 5, 9, 2, 1, 6],
 [6, 1, 2, 3, 8, 7, 4, 9, 5],
 [5, 4, 9, 2, 1, 6, 7, 3, 8],
 [7, 6, 3, 5, 2, 4, 1, 8, 9],
 [9, 2, 8, 6, 7, 1, 3, 5, 4],
 [1, 5, 4, 9, 3, 8, 6, 0, 0]]  -- 7 2

or we could have empty sudoku, which will give us all the valid sudokus possible in the world:

[[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, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0]]

Project Structure

This is created by using stack new suokdu

.
├── CHANGELOG.md
├── LICENSE
├── README.md
├── Setup.hs
├── app
│   └── Main.hs
├── emptySudoku.txt   -- empty sudoku input file.
├── package.yaml
├── src
│   └── Lib.hs
├── stack.yaml
├── stack.yaml.lock
├── sudoku.cabal
├── sudoku.txt        -- only one solution sudoku file.
└── test
    └── Spec.hs

Defining Types

here we will be defining a few of the types which will be used in our program, all of them are type aliases type instead of newtype as I wanted to use these types interchangeably with generic functions like crossProduct, which we will see later.

-- src/Lib.hs

type Cell = Int

type SudokuSize = Int

type Row = [Int]

type X = Int

type Y = Int

type Coord = (X, Y) -- Coordinates

type Sudoku = [Row]

Config

We will be storing some configs like sudokuSize and the gridSize in the Sudoku in the Lib.hs file.

-- src/Lib.hs

sudokuSize :: SudokuSize
sudokuSize = 9

blockSize :: Int
blockSize = 3

Parsing and printing Sudoku

We need to pretty print our sudoku in the terminal, so for that, we will be defining showSudoku function:

-- src/Lib.hs

showRow :: Show a => [a] -> String
showRow row = unwords $ show <$> row

showSudoku :: Show a => [[a]] -> String
showSudoku sudoku = unlines $ showRow <$> sudoku

and to parse our sudoku from the file we will define below the functions:

-- app/main.hs

module Main (main) where

import Data.Char (digitToInt)
import Control.Monad (replicateM)
import Lib (Cell, Row, sudokuSize, showSudoku)

parseChar :: Char -> Cell
parseChar '.' = 0
parseChar x = digitToInt x

parseString :: String -> Row
parseString = map parseChar

readRow :: IO Row
readRow = do
  row <- parseString <$> getLine
  if length row /= sudokuSize
    then error "row does not have the correct number of cells"
    else return row

main :: IO ()
main = do
    sudoku <- replicateM sudokuSize readRow
    mapM_ (putStrLn . showSudoku) [sudoku]

We have defined a function readRow which will read from stdin and try to parse it as a Row, and since readRow is an IO, i.e. it reads from the terminal, we can use replicateM to repeat this operation and read multiple rows from the terminal.

if we look at the type of replicateM in the stack repl, we will see that:

ghci> :t replicateM
replicateM :: Applicative m => Int -> m a -> m [a]

so here (Int -> m a) -> m [a] means that it will collect a from m type of computation and will return a new computation m [a] where all the a have been gathered from different computations. i.e. so here (9 -> readRow) replicateM needs a number 9 and a computation to replicate which is readRow, and it will return IO [Row] which is a sudoku.

similarly in MapM function, where _ will ignore the result:

ghci> :t mapM
mapM :: (Traversable t, Monad m) => (a -> m b) -> t a -> m (t b)
ghci> :t mapM_
mapM_ :: (Foldable t, Monad m) => (a -> m b) -> t a -> m ()

Validate Sudokus

for a sudoku to be valid, each row and column must contain a number from 1 to 9 exactly once; and also there are 9 grids of size 3x3 in a 9x9 sudoku, where no number must repeat.

get all rows

-- src/lib.hs

rows :: [[a]] -> [[[a]]]
rows = fmap (replicate sudokuSize)

get all columns

-- src/lib.hs

columns :: [[a]] -> [[[a]]]
columns = transpose . rows . transpose

get all grid blocks

to get all grid blocks we need to know which grid each element of sudoku belongs to, every grid has a start and end position which can be represented by Coord type.

Coordinates of all elements in sudoku:

-- src/lib.hs

coordinates :: [[Coord]]
coordinates = [[(r, c) | c <- [0 .. sudokuSize - 1]] | r <- [0 .. sudokuSize - 1]]

block coordinate will give us the start and end position pairs of all sudoku elements.

-- src/lib.hs

blockCoordinates :: [[(Coord, Coord)]]
blockCoordinates = (fmap . fmap) (\(x, y) -> (start x y, end x y)) coordinates
  where
    start x' y' = (3 * (x' `div` blockSize), 3 * (y' `div` blockSize))
    end x' y' = (\(x'', y'') -> (x'' + blockSize, y'' + blockSize)) $ start x' y'

if Sudoku type was parametrized like:

type Sudoku a = [[a]]

then we could have represented blockCoordinates as:

type Rectangle = (Coord, Coord) -- (start, end)
blockCoordinates :: [[Rectangle]]

to get values from block coordinates we need a few helper functions like slice and slice2D

-- src/lib.hs

slice :: Int -> Int -> [a] -> [a]
slice start end = drop start . take end
-- src/lib.hs

slice2D :: [[a]] -> Int -> Int -> Int -> Int -> [[a]]
slice2D sudoku startRow endRow startCol endCol = slice startRow endRow $ slice startCol endCol <$> sudoku

and finally, our function to get blocks at each coordinate:

-- src/lib.hs

blocks :: Sudoku -> [[[[Int]]]]
blocks sudoku = (fmap . fmap) block blockCoordinates
  where
    getSlice = slice2D sudoku
    block ((startRow, startCol), (endRow, endCol)) = getSlice startRow endRow startCol endCol

to understand it better, lets take our previous case where Sudoku was parametrized, which would give us

blocks :: Sudoku -> Sudoku (Sudoku Int)

here blocks is like a sudoku of sudokus, where inner sudoku is a 3x3 grid.

finally, we merge all the values of the rows, column and grid values at each coordinate of the grid and we get:

-- src/lib.hs

getAllValues :: Sudoku -> [[[Cell]]]
getAllValues sudoku = (fmap . fmap) (sort . nub) all'
  where
    add' = (zipWith . zipWith) (++)
    all' = add' blocks' $ add' rows' cols'
    rows' = rows sudoku
    cols' = columns sudoku
    blocks' = fmap concat <$> blocks sudoku

and alternate way would to see this would be if Suodku was parametrized

getAllValues :: Sudoku -> Sudoku [Int]

this tells us that each sudoku element is an array of integers.

and by this scenario, we can easily validate sudoku if all the elements of sudoku contain exactly 9 values

-- src/lib.hs

valid :: Sudoku -> Bool
valid sudoku = (all . all) (== 9) (fmap length <$> getAllValues sudoku)

Sudoku Solutions

getting solutions without context

The easiest solution would be to try 1 to 9 values for each 0 in the sudoku. the time complexity of this would be 9^n where n is the number of 0 in the sudoku. We already know that this solution is very slow, and we will never get any answer. But let’s see how it would be implemented in haskell.

first, we need a helper function that would give us all possible values for each element:

-- src/Lib.hs

possibilities :: Cell -> [Cell]
possibilities 0 = [1 .. 9]
possibilities n = [n]

then we need a way to cross-produce each possibility. Here cross-product is the same as the cross-product of a set in mathematics.

ghci> crossProduct [[1, 2, 3], [4, 5], [6]]
[[1,4,6],[1,5,6],[2,4,6],[2,5,6],[3,4,6],[3,5,6]]

and here is its implementation:

-- src/Lib.hs

crossProduct :: [[a]] -> [[a]]
crossProduct []             = []
crossProduct [a]            = [[x] | x <- a]
crossProduct (array : rest) = (:) <$> array <*> crossProduct rest

we could also use the sequence function from Prelude, which does the same thing.

ghci> :t sequence
sequence :: (Traversable t, Monad m) => t (m a) -> m (t a)
ghci> sequence [[1,2,3], [4,5], [6]]
[[1,4,6],[1,5,6],[2,4,6],[2,5,6],[3,4,6],[3,5,6]]

So by getting a cross-product of each possibility we will get all solutions available.

-- src/Lib.hs

getSolutions :: Sudoku -> [Sudoku]
getSolutions sudoku = filter valid allSudokus
  where
    allSudokus = crossProduct (crossProduct . fmap possibilities <$> sudoku)

so for this kind of solution, we have these many possibilities:

ghci> product $ fmap product $ fmap (length . possibilities) <$> sudoku
8599843895832833305

Which is a lot…

getting solutions based on context

now, we can be smarter about this and only generate possibilities which are not already present in the row, column and grid blocks.

-- src/Lib.hs

possibilitiesWithContext :: Sudoku -> Coord -> [Cell]
possibilitiesWithContext sudoku coord = if currentValue == 0 then possibleValues else [currentValue]
  where (x, y) = coord
        currentValue = head $ concat $ slice2D sudoku x (x + 1) y (y + 1)                          -- sudoku !! x !! y
        allValues' = getAllValues sudoku
        allValues = concatMap concat $ slice2D allValues' x (x + 1) y (y + 1)
        possibleValues = [0..sudokuSize] \\ allValues                                              -- subtract a from b | `a` \\ `b`

and will be plugging it in the getSolutions approach.

-- src/Lib.hs

getSolutions :: Sudoku -> [Sudoku]
getSolutions sudoku = filter valid allSudokus
  where
    possibilities' = possibilitiesWithContext sudoku
    allSudokus = crossProduct (crossProduct . fmap possibilities' <$> coordinates)
ghci> product $ fmap product $ fmap (length . possibilities') <$> coordinates
2972033482752

for this solution, almost 1000000 times fewer searches have to be done for this; but still slow for our computer.

generating solutions based on context

We can have a faster solution if for each coordinate with 0 we take one possible number and try to generate possibilities for other holes in the sudoku, if at some point we reach a hole in the sudoku where there is no possible number, then we backtrack to the previous hole and try next possibility, once we are done will all the holes in the sudoku, we return our solution.

for this approach, we would need a helper function that would replace our sudoku with given coordinates and a new value.

-- src/Lib.hs

replaceAt :: Int -> (a -> a) -> [a] -> [a]                -- works for 1D array
replaceAt index f array = left ++ (f current : right')
  where (left, right) = splitAt index array
        current = head right
        right' = tail right

and so finally our solution

-- src/Lib.hs

generateSudoku :: [Coord] -> Sudoku -> [Sudoku]
generateSudoku [] sudoku' = do
          guard (valid sudoku')
          return sudoku'
generateSudoku (coord: coords) sudoku' =  do
  let (x, y) = coord
  let values = possibilitiesWithContext sudoku' coord
  guard $ (not . null) values
  val <- values
  let sudoku'' = replaceAt x (replaceAt y (const val)) sudoku'
  generateSudoku coords sudoku''

Let’s break it down. coords is a list of coordinates where we have holes = 0. generateSudoku is a function that will try to replace coord with a possibility and will backtrack if no possibility is present at some point using guard.

and let’s plug it into our solution

-- src/Lib.hs

getSolutions :: Sudoku -> [Sudoku]
getSolutions sudoku = generateSudoku coords' sudoku
  where coords' = concatMap (fmap filter id ifValid) coordinates -- concat $ (fmap.fmap filter id) ifValid coordinates
        ifValid (x, y) = 0 == (sudoku !! x !! y)

Printing all solutions

so finally our main function looks like:

-- app/Main.hs

main :: IO ()
main = do
    sudoku <- replicateM sudokuSize readRow
    mapM_ (putStrLn . showSudoku) $ getSolutions sudoku

and our solution is

9 2 6 3 4 5 7 8 1
8 5 1 7 2 6 4 3 9
4 7 3 8 9 1 5 2 6
5 6 8 9 1 3 2 4 7
3 4 2 6 8 7 1 9 5
1 9 7 2 5 4 8 6 3
6 8 5 4 7 9 3 1 2
7 3 4 1 6 2 9 5 8
2 1 9 5 3 8 6 7 4

Github Repository: https://github.com/upendra1997/sudoku-haskell