import Data.Bits (setBit, testBit)
type Snake = String
snake :: Snake
= "bwb/w/b/wb/w/bw/b/w/bw/bw/b/w/b/wb/wb/wb/wb" snake
This represents the snake that we’re trying to fit into a cube. ‘b’ and ‘w’ represent black and white blocks, and ‘/’ represents a change of direction. The only thing that matters for the solution is the location of the direction changes in this list; the ‘b’ and ‘w’ just make it easier to check that the representation matches the real object.
type Cube = Int
= 0 emptyCube
This represents the state of the cube into which we’re trying to fit the snake. For a backtracking search, all we really need to record here is which of the 3x3x3 cells in the cube have already been filled - so that we don’t produce solutions where the snake intersects itself. So we can use the first 27 bits of an Int.
Now we’re ready to define functions for working with the cube: one to mark a cell as filled and one to see whether it’s filled.
bitIndex :: Position -> Int
= x*9 + y*3 + z
bitIndex (x, y, z) fill :: Cube -> Position -> Cube
= setBit cube (bitIndex pos)
fill cube pos isFilledAt :: Cube -> Position -> Bool
= testBit cube (bitIndex pos) isFilledAt cube pos
Our algorithm will work its way through the snake and the cube simultaneously, marking cells in the cube as used as it goes. It will need to know where it is in the cube, and in what direction it’s working.
type Position = (Int, Int, Int)
data Axis = X | Y | Z deriving (Eq, Show)
data Sign = Positive | Negative deriving (Eq, Show)
data Direction = Direction Axis Sign
If the algorithm encounters a change-direction marker in the snake then it can go in any of the four directions that are perpendicular to the current direction.
nextDirections :: Direction -> [Direction]
Direction a _) = [Direction a' d |
nextDirections (<- [X, Y, Z],
a' /= a,
a' <- [Positive, Negative]] d
Otherwise, it will just move to the next cell, in the direction in which it’s already heading.
nextPosition :: Position -> Direction -> Position
Direction a d) =
nextPosition (x, y, z) (case a
of X -> (x+delta, y, z)
Y -> (x, y+delta, z)
Z -> (x, y, z+delta)
where delta = if d == Positive then 1 else -1
We’ll need to know when we’ve reached the edge of the space.
outOfBounds :: Position -> Bool
= or [i < 0 || i > 2 | i <- [x, y, z]] outOfBounds (x, y, z)
Now we have all the parts we need to put together a search algorithm. We’ll build up a list of the directions in which the segments of the snake need to lie; that’s enough information to follow and it’s easier to interpret than, say, a list of the cell coordinates.
type PartialSolution = [Direction]
type Solution = PartialSolution
Here’s the core of the algorithm: a function that consumes one character of the snake description, works out what possible next steps there are, and collects the solutions found by each of those steps. At each step, it needs to know:
- which cells of the cube have already been filled;
- what the last-filled position and direction are;
- what part of the snake remains to be placed in the cube;
- what solution has been accumulated so far.
solutions' :: Cube
-> Position
-> Direction
-> Snake
-> PartialSolution
-> [Solution]
If there’s nothing of the snake left to place, then we’ve found one solution. This is the base case for the recursive search.
= [dir:soFar] solutions' _ _ dir [] soFar
If the next character in the snake indicates a change of direction, then we have to look for solutions in each of the four possible next directions. We don’t change position or mark any cell as filled, but we’ll need to include the change of direction in any solutions we find.
/':snake') soFar =
solutions' cube prevPos dir ('concat [solutions' cube prevPos dir' snake' (dir:soFar) |
<- nextDirections dir] dir'
Otherwise, we must mark our position as filled - or return empty-handed if that’s impossible - and move to the next cell, in the same direction as before.
:snake') soFar =
solutions' cube prevPos dir (clet pos = nextPosition prevPos dir in
if outOfBounds pos || cube `isFilledAt` pos then []
else solutions' (fill cube pos) pos dir snake' soFar
A small wrapper describes the starting conditions for the search.
= map reverse $ solutions' emptyCube
solutions snake -1, 0, 0)
(Direction X Positive)
(
snake []
Finally, we will show the directions in a solution as crude arrows.
instance Show Direction where
show (Direction X Positive) = ">"
show (Direction X Negative) = "<"
show (Direction Y Positive) = "^"
show (Direction Y Negative) = "v"
show (Direction Z Positive) = "."
show (Direction Z Negative) = "o"
= mapM_ putStrLn $ map (unwords . map show) $ solutions snake main
We get just two solutions:
> ^ < . ^ o > . v < ^ o ^ . v > ^
> . < ^ . v > ^ o < . v . ^ o > .
which are actually just the unique solution and its mirror image.