By Eric Burden | December 4, 2021
It’s that time of year again! Just like last year, I’ll be posting my solutions to the Advent of Code puzzles. This year, I’ll be solving the puzzles in Julia. I’ll post my solutions and code to GitHub as well. I had a blast with AoC last year, first in R, then as a set of exercises for learning Rust, so I’m really excited to see what this year’s puzzles will bring. If you haven’t given AoC a try, I encourage you to do so along with me!
Day 4 - Giant Squid
Find the problem description HERE.
The Input - Board Stiff
I refuse to discuss this “Input” section any further. It’s here. It’s probably
going to be here for a while. Deal with it. (See my posts for previous days if
you’re curious why I’d be slightly salty about needing this section). For
today’s puzzle, the input parsing does like 90% of the heavy lifting. A quick
read of the instructions tells me that I’m going to need Bingo Board
s, and
that I’ll need to be able to “mark off” numbers in a sequence and know when
a particular Board
has accumulated five marked off numbers in a vertical
or horizontal line. Importantly, diagonal lines are specifically excluded, which
was a bummer because I specifically included them in my first pass at this.
I know how Bingo works! Who needs directions? - Me
I’m using a Dict in each Board
to map values to their positions on the board,
to make number lookups O(1). I’m using a BitArray
the same shape as the board
values for “marking off” numbers as they’re called, and I’m using a fancy Dict
to tell when a full line has been marked off, creating a winning board.
# Used to indicate whether a given line index is for a row
# or a column
abstract type AbstractIndexType end
struct RowIndex <: AbstractIndexType idx::Int end
struct ColIndex <: AbstractIndexType idx::Int end
# A struct to represent the state of an individual bingo board
# Contains fields for:
# - indexmap: A Dict whose keys are values in the board and whose
# values are the index of that board value
# - numbers: A 2D matrix of the board values
# - found: A mask for `numbers` to indicate which board numbers
# have been called
# - linecounts: A Dict used to determine when a full column or row
# has been drawn. The keys are an AbstractIndexType
# indicating the line(s) of the number on the board.
# The values are the count of numbers in that
# row/column that have been drawn.
mutable struct Board
indexmap::Dict{Int, Int}
numbers::Matrix{Int}
found::BitMatrix
linecounts::Dict{AbstractIndexType, Int}
end
# Constructor for a `Board`, generates the other fields from `numbers`
function Board(numbers::Matrix{Int})
# Build the `indexmap`
mapnumbers(n) = map(i -> (numbers[i], i), n)
(indexmap
= numbers
|> eachindex
|> mapnumbers
|> Dict)
found = falses(size(numbers))
linecounts = Dict()
Board(indexmap, numbers, found, linecounts)
end
# Call a number for a particular board. As in bingo, when a number
# is called, it should be marked on the board. This function
# marks which number was called in the board's `found` BitMatrix
# and updates the board's `linecounts` . If playing that number
# fills out a row or column, return `true`, otherwise return `false`.
function play!(number::Int, board::Board)::Bool
haskey(board.indexmap, number) || return false
idx = get(board.indexmap, number, 0)
board.found[idx] = true
(row, col) = Tuple(CartesianIndices(board.numbers)[idx])
boardwins = false
linecountkeys = [RowIndex(row), ColIndex(col)]
for key in linecountkeys
linecount = get(board.linecounts, key, 0)
board.linecounts[key] = linecount + 1
if linecount + 1 >= 5; boardwins = true; end
end
return boardwins
end
# Read in and parse the contents of the input file
function ingest(path)
open(inputpath) do f
# Read the first line of numbers and parse into
# an array of Ints
numstr = readuntil(f, "\n\n")
numbers = [parse(Int, s) for s in split(numstr, ",")]
# Utility functions to help with parsing
mergevectors(x) = reduce(hcat, x)
alltoint(x) = parse.(Int, x)
intoboard = Board ∘ collect ∘ transpose
boards = []
# Until the end of the file, read up to the next empty line,
# split the lines, merge into a Matrix, convert all the Matrix
# strings to integers, transpose the Matrix, then pass it to
# the Board constructor.
while !eof(f)
boardstr = readuntil(f, "\n\n")
(boardnums
= [split(s) for s in split(boardstr, "\n")]
|> mergevectors
|> alltoint
|> intoboard)
push!(boards, boardnums)
end
(numbers, boards) # Return numbers and boards in a Tuple
end
end
You can use the same method with the linecounts
Dict for determining when
diagonal lines have been made too, but since the puzzle forbids it, I won’t
go into it (if you’re curious, let me know in the comments).
Part One - Squid Game
Ok…. We’re under attack by a giant squid, and our solution is to play Bingo with it. Cool. Makes sense. At least we’re willing to cheat our way through it!
# Play each number on each board until a winner is found, then
# return the sum of the winning board's unmarked numbers times
# the last number drawn.
function part1(numbers, boards)
for number in numbers
for board in boards
if play!(number, board)
unmarked = board.numbers[.!board.found]
return sum(unmarked) * number
end
end
end
error("Could not find a winning board!")
end
There’s not much to say about this code, other that you need to pass part1()
a copy of a list of Board
s if you want to use that list for anything else
later, since the Board
s are mutated directly. Otherwise, this does exactly
what the comment says it does. Note that play!()
return true if marking the
given number for the given board creates a 5-in-a-row for that board, and false
otherwise.
Part Two - There Can Be Only One
In part two, we come to our senses. No, we didn’t rig up any actual defenses against the squid, but we did decide to cheat in its favor at Bingo. Now we need to identify the last board to win and make sure we get that one.
using DataStructures
# Play each number on each board until all winners are found, then
# return the sum of the last winning board's unmarked numbers times
# the last number drawn.
function part2(numbers, boards)
# Convert the Vector of boards into an OrderedSet to allow for
# convenient iteration in order and easy removal of boards from
# the set once they "win"
boardset = OrderedSet{Board}(boards)
for number in numbers
for board in boardset
if play!(number, board)
# Return for the last winning board
if length(boardset) == 1
unmarked = board.numbers[.!board.found]
return sum(unmarked) * number
end
# Remove any winning board from the OrderedSet
delete!(boardset, board)
end
end
end
error("Could not find a winning board!")
end
This code is very much like the code for part one, with the twist of using an
OrderedSet
for our Boards
instead of a Vector
. This way, we can iterate
through the Boards
in order, while also quickly and easily nixing the winning
boards one at a time until only one remains.
Wrap Up
Yesterday, I remarked on some R-like features of Julia that I really enjoyed. Today, I had fun tinkering with some more Rust-like features in using Types to specify row/column indices and in the more OO-style syntax of structs. Granted, structs aren’t particularly object-oriented, but they’re a lot closer than S3 or S4 objects, in my opinion (if you don’t know what that is, don’t worry about it). I do find myself missing Traits (from Rust), but I’m slowly getting used to Julie’s “multiple dispatch” model. The file parsing functions available in the Julia standard library were super nice to use, too. I know I’ll miss those when I’m working in R or Rust. All in all, a really solid day.
If you found a different solution (or spotted a mistake in one of mine), please drop me a line!