Imagine a 6 by 6 grid of squares, that can either be black or white.

It has to fulfill the following properties:
1. Each row and column needs to have 3 white and 3 black squares.
2. All black squares have to be orthogonally connected.

Prove that such a grid cannot exist.
  • part 1: naive brute force exhaustive search
  • part 2: smarter exhaustive search
  • part 3: inductive graphs
  • part 4: connected magic squares
  • part 5: experiments

Note: in our digital representation black squares are ones and white squares are zeros.

This is part 4 of a series of posts about a puzzle I found on Mastodon and which I don’t yet know how to solve. In part 1 we decided to employ a brute force exhaustive search for solutions. In part 2 we improved the performance of the brute force search so it can handle magic squares of size six by six. In part 3 we tackled graph algorithms in a functional programming setting.

We’re now ready to put it all together and run some experiments. What is left to do is to convert a row matrix into an inductive graph and run a DFS over it.

Our strategy will be simple: scan the matrix one row at a time and only look “back” and “up” for connections (so previous row, same column or same row previous column). This ensures that the current node we’re working on and attaching to the graph only references previous nodes already attached to the graph in its adjacency list. That’s it. To get started, we probably need an index-based access to previous matrix values, so it would be useful to have an index-based form of it handy too. The following two functions do the conversion:

listToArray :: [a] -> Array Int a
listToArray xs = array idxs (zip (range idxs) xs)
  where
    idxs = (0, (length xs - 1))

matrixToArray :: [[a]] -> Array Int (Array Int a)
matrixToArray m = listToArray rs
  where
   rs = map listToArray m

We also want a function that given a row, column tuple makes an integer out of it. And also its inverse.

rowColumnToNode :: Int -> (Int, Int) -> Int
rowColumnToNode size (row, column) = row * size + column

nodeToRowColumn :: Int -> Int -> (Int, Int)
nodeToRowColumn size node = (div node size, mod node size)

We’ll use a list comprehension for the (row, column) tuples and foldr over it to create the inductive graph. Let’s check the order of our list comprehension bounded variables:

ghci> [(i, j) | i <- [0..2], j <- [0..2]]
[(0,0),(0,1),(0,2),(1,0),(1,1),(1,2),(2,0),(2,1),(2,2)]

Since we’re doing a foldr it would start adding from the back of that list. We already decided to look to previous and I’m lazy, so let’s keep that and adjust the list comprehension:

ghci> [(2 - i, 2 - j) | i <- [0..2], j <- [0..2]]
[(2,2),(2,1),(2,0),(1,2),(1,1),(1,0),(0,2),(0,1),(0,0)]

We still need the function we will foldr with. We’re going to curry the matrix size and indexed array matrix representation into it.

type MatrixArray = Array Int (Array Int Int)

matrixToGraphHelper' :: Int -> MatrixArray -> (Int, Int) -> Graph -> Graph
matrixToGraphHelper' size ma (row, column) g = ctx:g 
      where
        node = rowColumnToNode size (row, column)
        orthoNorthNeighbor :: (Int, Int) -> [Int] -> [Int]
        orthoNorthNeighbor (r, c) ns = if row > 0 then 
              if (ma ! (r - 1) ! c)  == 1 then 
                  (rowColumnToNode size (r -1, c)):ns 
              else ns
          else ns
        orthoWestNeighbor :: (Int, Int) -> [Int] -> [Int]
        orthoWestNeighbor  (r, c) ns = if c > 0 then 
                      if (ma ! r ! (c - 1))  == 1 then 
                         rowColumnToNode size (r, (c-1)):ns 
                      else ns
                  else ns
        adj = orthoNorthNeighbor (row, column) (orthoWestNeighbor (row, column) [])
        ctx = (node, adj)


matrixToGraphHelper :: Int -> MatrixArray -> (Int, Int) -> Graph -> Graph
matrixToGraphHelper size ma (row, column) g = 
     if (ma ! row ! column) == 1 then matrixToGraphHelper' size ma (row, column) g else g

matrixToGraph :: Int -> MatrixArray -> Graph
matrixToGraph size ma = foldr f [] idxs
  where
    idxs = [(size - i - 1, size - j - 1) | i <- [0..(size-1)], j <- [0..(size-1)]]
    f = matrixToGraphHelper size ma

We are finally ready for a full implementation of solved that also checks for connectedness:

solved :: Int -> State -> Bool
solved k s = (length rows) == size && (allTrue (k ==) cs) && (length vs == length g)
  where 
    (rows, cs) = s
    size = length cs
    ma = matrixToArray rows
    g = matrixToGraph size ma
    sn = fst (head g)
    vs = dfs [sn] g

Let’s take it for a spin. We already know from the puzzle statement that six by six connected magic square is impossible, so we should get the empty list:

ghci> solutions 3 ([], [0, 0, 0, 0, 0, 0])
[]

what about with more number of ones ? The intuition here is that the chance of being connected increases when there are more ones available in each row and column. Turns out, there are 3108 solutions with four ones. We can plot one example solution:

6 by 6 square with 4 ones

In the next part we will use this computing machinery to tickle out examples and counter examples and maybe find something to latch onto.

Here is the complete, cleaned up code with all the unused functions from abandoned approaches deleted. You can also find it on this github repo.

module MagicSquare  (magicSquares, connectedSquares) where

import Data.Array
import Data.Bits

allTrue :: (a -> Bool) -> [a] -> Bool
allTrue f xs = foldr (&&) True (map f xs)

rowColumnToNode :: Int -> (Int, Int) -> Int
rowColumnToNode rows (row, column) = row * rows + column

nodeToRowColumn :: Int -> Int -> (Int, Int)
nodeToRowColumn rows vertex = (div vertex rows, mod vertex rows)

listToArray :: [a] -> Array Int a
listToArray xs = array idxs (zip (range idxs) xs)
  where
    idxs = (0, (length xs - 1))

matrixToArray :: [[a]] -> Array Int (Array Int a)
matrixToArray m = listToArray rs
  where
   rs = map listToArray m

kones :: [Bool] -> Int-> [[Int]]
kones cs 0 = [[0 | _ <- [1..n]]]
   where
    n = length cs
kones [] _ = []
kones (c:cs) k = if c then 
                          (map (1:) (kones cs (k-1))) ++ (map (0:) (kones cs k)) 
                      else 
                          (map (0:) (kones cs k))

type Node = Int
type Adj = [Node]
type Context = (Node, Adj)
type Graph = [Context]

without :: Node -> Adj -> Adj
without _ [] = []
without y (x:xs) = if x == y then xs else x:(without y xs)

findNode :: Node -> Graph -> Maybe (Int, Context)
findNode _ [] = Nothing
findNode u (v:g) = if u == fst v then Just (0, v) else 
        case (findNode u g) of
          Just (n, v) -> Just ((n+1), v)
          Nothing -> Nothing

transferEdge :: Context -> (Context, Graph) -> (Context, Graph)
transferEdge (v, onV) ((matched, onMatched), xs) = if (elem matched onV) then 
         ((matched, v:onMatched), (v, without matched onV):xs)
    else
         ((matched, onMatched), (v, onV): xs)

match :: Node -> Graph -> Maybe (Context, Graph)
match x xs = case idx of
    Just (n, c) -> 
        let
          prefix = take n xs
          (c', prefix') = foldr transferEdge (c, []) prefix
          suffix = drop (n+1) xs
        in
          Just (c', prefix' ++ suffix)
    Nothing -> Nothing
  where
    idx = findNode x xs

dfs :: [Node] -> Graph -> [Node]
dfs [] _ = []
dfs (v:vs) g = case m of
    Just (c, g') -> (fst c):(dfs ((snd c) ++ vs) g')
    Nothing -> dfs vs g
  where
    m = match v g

type MatrixArray = Array Int (Array Int Int)

matrixToGraphHelper' :: Int -> MatrixArray -> (Int, Int) -> Graph -> Graph
matrixToGraphHelper' size ma (row, column) g = ctx:g 
      where
        node = rowColumnToNode size (row, column)
        orthoNorthNeighbor :: (Int, Int) -> [Int] -> [Int]
        orthoNorthNeighbor (r, c) ns = if row > 0 then 
              if (ma ! (r - 1) ! c)  == 1 then 
                  (rowColumnToNode size (r -1, c)):ns 
              else ns
          else ns
        orthoWestNeighbor :: (Int, Int) -> [Int] -> [Int]
        orthoWestNeighbor  (r, c) ns = if c > 0 then 
                      if (ma ! r ! (c - 1))  == 1 then 
                         rowColumnToNode size (r, (c-1)):ns 
                      else ns
                  else ns
        adj = orthoNorthNeighbor (row, column) (orthoWestNeighbor (row, column) [])
        ctx = (node, adj)

matrixToGraphHelper :: Int -> MatrixArray -> (Int, Int) -> Graph -> Graph
matrixToGraphHelper size ma (row, column) g = 
     if (ma ! row ! column) == 1 then matrixToGraphHelper' size ma (row, column) g else g

matrixToGraph :: Int -> MatrixArray -> Graph
matrixToGraph size ma = foldr f [] idxs
  where
    idxs = [(size - i - 1, size - j - 1) | i <- [0..(size-1)], j <- [0..(size-1)]]
    f = matrixToGraphHelper size ma

type State = ([[Int]], [Int])
type Move = [Int]

moves :: Int -> State -> [Move]
moves k s = if ((length rows) == (length csum)) then [] else kones cs k
  where
   (rows, csum) = s
   cs = map ((>) k) csum

move :: State -> Move -> State
move s m = (m:rows, map (\x -> (fst x) + (snd x)) (zip m cs))
  where
    (rows, cs) = s

succs :: Int -> State -> [State]
succs k t = [move t m | m <- (moves k t)]

solvedMagic :: Int -> State -> Bool
solvedMagic k s = (length rows) == size && (allTrue (k ==) cs)
  where 
    (rows, cs) = s
    size = length cs
    
solvedConnected :: Int -> State -> Bool
solvedConnected k s = (length rows) == size && (allTrue (k ==) cs) && (length vs == length g)
  where 
    (rows, cs) = s
    size = length cs
    ma = matrixToArray rows
    g = matrixToGraph size ma
    sn = fst (head g)
    vs = dfs [sn] g

search :: (Int -> State -> Bool) -> Int -> [State] -> [State]
search _ _ [] = []
search solved k (t:ts) = if (solved k t) then t:(search solved k ts) else search solved k (succs k t ++ ts)

solutions :: (Int -> State -> Bool) -> Int -> State -> [State]
solutions solved k t = search solved k [t]

magicSquares :: Int -> Int -> [[[Int]]]
magicSquares size numOnes = map fst (solutions solvedMagic numOnes ([], [0::Int | _ <- [1..size]]))

connectedSquares :: Int -> Int -> [[[Int]]]
connectedSquares size numOnes = map fst (solutions solvedConnected numOnes ([], [0::Int | _ <- [1..size]]))