James Adam Buckland
Home Blog Games Resume

2017-01-22 Solving One Tough Puzzle math puzzle

The Puzzle

Photocredit to https://spabettie.files.wordpress.com

One Tough Puzzle is a physical jigsaw puzzle made up of nine square pieces. Each side of the square as either a tab or a blank in the shape of one of the four card suits. The goal is to put them all together so they fit, like any jigsaw puzzle.

I will use this post as a tour of some cool Haskell features I’ve been enjoying for the past few months doing Project Euler problems.

The Problem

We number the spaces like so:

+-----------+
| 0 | 1 | 2 |
+-----------+
| 3 | 4 | 5 |
+-----------+
| 6 | 7 | 8 |
+-----------+

Nine pieces in nine slots gives us $9! = 362880$ positional combinations, and nine pieces which can be rotated any of four ways gives us $4^9 = 262144$ rotational combinations, for a total of just over $23\times 10^9$ combinations (accounting for rotational symmetry). Too many to brute force.

The Solution

We can instead try to solve the problem a little bit at a time. Our algorithm will be as follows:

This has the advantage of trimming the search space as quickly as possible, to avoid having to validate all twenty-three billion possible grids. We will check in at the end and see how many grids or fractions thereof we actually did have to validate.

Using Haskell

My language of choice for this is Haskell, mostly because I’ve been doing Project Euler problems in it recently and I really like the custom data types and flexibility.

data

Haskell has the ability to define custom data types, and compose and define them in interesting ways. In this puzzle we want to be able to inspect and compare tabs and blanks, puzzle pieces, etc.

data Suit = Club | Heart | Diamond | Spade           deriving (Show, Read, Eq)
data Sex  = Out | In                                 deriving (Show, Read, Eq)
data Side = Side { suit :: Suit , sex :: Sex}        deriving (Show, Read)
data Piece = Piece { north :: Side , east  :: Side
                   , south :: Side , west  :: Side } deriving (Show, Read, Eq)

deriving after a custom data type tells Haskell to infer how to print (show), read, or compare variables of that type. Comparisons can be

In our case, we don’t really have a way to decide whether one Suit or Sex is “bigger” than another, but we do want to check equality.

We define Side and Piece with named fields, which lets us extract those fields with generated getters:

λ> let foo = Side {suit=Club, sex=Out}
λ> suit foo
Club

For two puzzle pieces to be able to meet at an edge, they need to have the same Suit and opposite Sex es. This makes it convenient to define a custom instance of Eq for Side variables. This lets us redefine a == b to check whether two sides are compatible, rather than equal.

instance Eq Side where x == y = (suit x == suit y) && (sex x /= sex y)

Input

I defined the nine pieces in plaintext as follows:

[ "HDdh", "CHsh", "DCcd", "SDsh", "SDhd", "SShc", "CHdc", "HDcc", "HSsc"]

where each character is the tab or blank on the North, East, South, and West faces of a piece. Each letter stands for Heart, Diamond, Spade, or Club, and is uppercase if the side sticks out rather than in.

We have to write a bit of parsing code to turn this a list of Piece objects.

parsePiece [n,e,s,w] = Piece { north = parseSide n, east = parseSide e           
                             , south = parseSide s, west = parseSide w }         
  where parseSide c = Side { suit = parseSuit c, sex =  parseSex c }             
        parseSuit c | toLower c == 'c' = Club                                    
                    | toLower c == 'd' = Diamond                                 
                    | toLower c == 's' = Spade                                   
                    | otherwise        = Heart                                   
        parseSex  c = if isLower c then In else Out                              

Later, we can call

let allPieces = map parsePiece $ [ "HDdh", "CHsh", ... ]

explore

Theory

At the core of this solution is a function I named explore, which takes a known grid and a list of candidates and generates a list of potential grids and their remaining candidates. We represent a grid as a list of pieces [Piece].

explore :: ([Piece], [Piece]) -> [([Piece], [Piece])]

Here is an example of explore in action.

--          known grid
--          |        candidates
--          |        |
--          v        v
λ> explore ([1, 2], [3, 4, 5])
[ ([1, 2, 3], [4, 5]),
  ([1, 2, 4], [3, 5]),
  ([1, 2, 5], [3, 4]) ]

One caveat here is that we don’t just append all the items in the candidates pool to the known grid; because these are puzzle pieces, we need to append all possible rotations of each candidate. So a closer approximation with lower- and upper-case letters might be:

λ> explore ([], ['c', 'd', 'e'])
[ (['c'], ['d', 'e']),
  (['C'], ['d', 'e']),
  (['d'], ['c', 'e']),
  (['D'], ['c', 'e']),
  ... ]

Once we have this explore function, we can validate the list it generates, and then call explore on every remaining grid again. We can repeat this until we are left with complete, valid grids.

Practice

Here’s what the real explore function looks like:

explore :: ([Piece], [Piece]) -> [([Piece], [Piece])]
explore (list, pool) = concatMap pluck [0..(length pool - 1)]
  where pluck i = [ (list ++ [c], excise i pool)
                  | c <- take 4 $ iterate rotate (pool!!i) ]
        excise i xs = take i xs ++ drop (i+1) xs
        rotate piece = Piece { north = east piece, east = south piece
                             , south = west piece, west = north piece }

We define a few helper functions which are pretty useful.

validate

We need to validate each grid or portion of a grid which is generated in an efficient way. If we add pieces in slots zero through nine in order, then each new piece only needs to be compared against the pieces above or to the left of it (if they exist). Thus:

validate :: Int -> [Piece] -> Bool
validate n xs = (not hasAbove || matchAbove) && (not hasLeft || matchLeft)
  where hasLeft    = n `mod` 3 /= 0
        hasAbove   = n >= 3
        matchLeft  = west  (xs!!n) == east  (xs!!(n-1))
        matchAbove = north (xs!!n) == south (xs!!(n-3))

matchLeft and matchAbove both take advantage of our custom instance of Eq for Sides. The getters north, east, south, and west were also generated implicitly from the named fields above.

OK. Let’s put it together.

Finding the solution

step 0 xs = explore ([], xs)
step n xs = filter (\(xs,_) -> validate n xs)
          $ concatMap explore $ step (pred n) xs
main = do
  let allPieces = map parsePiece
                $ [ "HDdh", "CHsh", "DCcd",
                    "SDsh", "SDhd", "SShc",
                    "CHdc", "HDcc", "HSsc" ]
  let sol = fst $ head $ step 8 allPieces

Cool. We can get our list of potential position 0 s easily with step 0 allPieces.

We can then get our list of potential position 1 s with:

step 1 allPieces
-- which turns into
filter (\(xs, _) -> validate 1 xs)) $ concatMap explore $ step 0 xs

This recursion means we validate a list of potential grids before returning it. So, step 8 allPieces generates the list of all potential 8th-poisition grids, which requires Haskell to generate the list of all potential 7th-position grids first, and so on and so on.

Filtering

How much does this save us? Let’s look at the numbers.

λ> print $ length $ step 0 allPieces
36
λ> print $ length $ step 1 allPieces
138
λ> print $ length $ step 2 allPieces
470
λ> print $ length $ step 3 allPieces
1350
λ> print $ length $ step 4 allPieces
474
λ> print $ length $ step 5 allPieces
144
λ> print $ length $ step 6 allPieces
175
λ> print $ length $ step 7 allPieces
28
λ> print $ length $ step 8 allPieces
4

One note – some of these numbers will be off by a factor of four, since a solved grid is the same across any rotation. That’s why there are $4$ valid solutions in the last row – it’s all the same solution across four rotations.

This means there were 36 valid pieces in position 0 (makes sense, since $9\times4$), but only 138 valid pairings for slots 0 and 1 adjacent, far less than the 1152 potentials. We peak at 1350 potential three-in-a-row combinations, and then filter down steadily.

Summing across these numbers, we see that validate must have been called something like 2800 times. Much, much better than validating all 23 billion grids.

Printing

Finally, we define a solution grid like so:

let sol = fst $ head $ step 8 allPieces

Unfortunately this gives us an output like this:

λ> main --solves the puzzle, returns the first valid grid.
[Piece {north = Side {suit = Heart, sex = In}, east = Side {suit = ...

Hard to read and inspect visually. I wrote a prettyprinter which turns that into this:

λ> main
.h. | .c. | .d.
s S | s H | h S
.D. | .S. | .D.
----|-----|----
.d. | .s. | .d.
D h | H h | H c
.H. | .C. | .C.
----|-----|----
.h. | .c. | .c.
S c | C d | D c
.S. | .D. | .H.

Which is much easier to inspect and validate. Here’s the prettyprinter:

renderGrid :: [Piece] -> [String]
renderGrid = intercalate ["----|-----|----"] . map renderRow . chunksOf 3
 where renderRow xs = [ intercalate " | " $ map (\x -> renderSq x!!n) xs
                      | n <- [0..2] ]
       renderSq p = [ "."          ++ [unParse $ north p] ++ "."
                    , [unParse $ west p] ++ " "           ++ [unParse $ east p]
                    , "."          ++ [unParse $ south p] ++ "."
                    ]
       unParse s
         | suit s == Club    = f 'c'
         | suit s == Spade   = f 's'
         | suit s == Diamond = f 'd'
         | otherwise         = f 'h'
         where f = if sex s == Out then toUpper else id

Understanding this particular function is left as an exercise to the reader.

Full Code

You can see the full code here, in a gist.