Solutions and Write-Ups for my Advent Of Code adventures (mainly in Haskell)
I might have gone a bit overboard with this one lol.
The input is just a list of three-tuples. Haskell already knows how to parse that, as long as there are parenthesis around them :)
type Position = (Int, Int, Int)
type Input = [Position]
type Output = Int
parseInput :: String -> Input
parseInput = map (\x -> read $ "(" ++ x ++ ")") . lines
We want to connect the 1000 closest pairs of numbers together and multiply the sizes of the three biggest components that this creates.
We have two subproblems here:
Let’s start with subproblem 1.
This is pretty simple to do in Haskell, here is a way of doing it (albeit unefficient).
distance :: Position -> Position -> Int
distance (x1, y1, z1) (x2, y2, z2) = x * x + y * y + z * z
where x = x1 - x2
y = y1 - y2
z = z1 - z2
closestPairs :: Input -> [(Position, Position)]
closestPairs boxes = sortBy (comparing $ uncurry distance)
[(b1, b2) | b1 <- boxes, b2 <- boxes, b1 < b2]
distance simply computes the distance (squared) between two positions.
closestPairs goes through every ordered pair and sorts them by their distance.
Now, subproblem number two is pretty fun: the real challenge isn’t in the algorithm, but in the data representation.
The algorithm itself is pretty simple:
The real problem comes to the way we represent the graph:
Here, I chose the latter. I made a small data structure that I called component map:
-- A data structure representing the connected components of an
-- undirected graph, without storing any of the graph’s edges.
data ComponentMap k = ComponentMap
{ idMap :: M.Map k Int
, sizeMap :: M.Map Int Int
} deriving (Show, Eq)
Technically speaking, sizeMap can be derived from idMap, but I think having a fast O(log n) access to the sizes is nice.
In a component map, I can merge two components. That is:
mergeComponents :: Ord k => ComponentMap k -> Int -> Int -> ComponentMap k
mergeComponents (ComponentMap mIds mSize) id1 id2 = ComponentMap mIds' mSize'
where (idMerge, idVoid) = (min id1 id2, max id1 id2)
mIds' = M.map (\x -> if x == idVoid then idMerge else x) mIds
mSize' = M.insert idVoid 0
. M.adjust (\x -> x + (mSize ! idVoid)) idMerge
$ mSize
(I could and should actually delete component idVoid here, but it makes it simple to simply “empty” it as it means that I can just use Size mSize for new IDs when inserting edges)
Now, in order to insert a new edges there are four possible cases:
insertEdge :: Ord k => ComponentMap k -> (k, k) -> ComponentMap k
insertEdge cMap@(ComponentMap mIds mSize) (a, b) =
case (mIds !? a, mIds !? b) of
(Nothing, Nothing) ->
let newID = M.size mSize
mIds' = foldl (\m p -> M.insert p newID m) mIds [a, b]
mSize' = M.insert newID 2 mSize
in ComponentMap mIds' mSize'
(Just idVal, Nothing) ->
ComponentMap
(M.insert b idVal mIds)
(M.adjust (+ 1) idVal mSize)
(Nothing, Just idVal) ->
ComponentMap
(M.insert a idVal mIds)
(M.adjust (+ 1) idVal mSize)
(Just id1, Just id2)
| id1 == id2 -> cMap
| otherwise -> mergeComponents cMap id1 id2
With that, part one is just a question of inserting the first 1000 pairs, then taking the 3 biggest components and multiplying their sizes:
partOne :: Input -> Output
partOne = product . take 3 . sortBy (comparing Down) . M.elems . sizeMap
. foldl insertEdge (ComponentMap M.empty M.empty)
. take 1000 . closestPairs
We want to insert edges until we just have one single component containing all the nodes.
One nice thing I did in my mergeComponents function is that I’m always using the smallest ID as the merging component.
This means that we can just insertEdges until the size of component 0 is equal to the number of positions.
When we have that, we know exactly which edge caused this:
partTwo :: Input -> Output
partTwo boxes = x1 * x2
where numBoxes = length boxes
((x1, _, _), (x2, _, _)) = closestPairs boxes !! (n - 1)
n = length
. takeWhile (\m -> sizeMap m !? 0 /= Just numBoxes)
. scanl insertEdge (ComponentMap M.empty M.empty)
$ closestPairs boxes
This was technically overkilled. Lists would have done the trick.
But maybe I’ll reuse this data structure some day, who knows?