Advent of Code 2021 – Day 4

By: Julia on Eric Burden

Re-posted from: https://ericburden.work/blog/2021/12/04/advent-of-code-2021-day-4/

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 Boards, 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 Boards if you want to use that list for anything else
later, since the Boards 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!

Advent of Code 2021 – Day 4

By: Julia on Eric Burden

Re-posted from: https://www.ericburden.work/blog/2021/12/04/advent-of-code-2021-day-4/

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.

Choosing how to store your stings

By: Blog by Bogumił Kamiński

Re-posted from: https://bkamins.github.io/julialang/2021/12/03/strings.html

Introduction

When you work with strings in Julia you have several options how to store them.
In this post I discuss the most common usage scenarios and the recommended
choices.

The sting storage decision tree

The choices I discuss below are related to performance and memory consumption.
Therefore in what follows I assume you work with large data sets relative to
available RAM on your machine.

Let me start with the decision flowchart and then explain it:

String decision guideline flowchart

The first decision you need to make is if you want only performance optimization
or you want your strings to be treated as ordered or unordered categorical data
in statistical sense. If you need your data to be categorical then the choice
is simple. The only option is to use the CategoricalArrays.jl package,
where the underlying data can be stored in a String.

On the other hand if your goal is only performance and saving memory then the
first question is if the number of unique values of strings in your data is
low. If this is the case then the recommended package is PooledArrays.jl, where again you should be fine with storing String values.

We are down to the scenario when you have a lot of strings that have very many
unique values. In such a case the question is if all of these strings are
relatively short and have a similar size. If this is the case then you can use
the InlineStrings.jl package. It provides several types called String1,
String3, String7, String15 etc. where the number indicates maximum string
size in bytes that a given type can store. The benefit of these values is that
they are not heap allocated. It means that they are fast to work with and they
do not burden the Julia Garbage Collector.

Finally we are left with many strings, that have many unique values and that
have varying and possibly large size. In this case what Julia Base offers is a
sensible choice. Normally you should just use the String type stored in
standard collections like Vector. However, there is one special case when
you could consider using the Symbol type instead of a String type.
You can use Symbol instead of String if all the following conditions are
met:

  • your strings are just labels that you only need to compare against each other;
    in particular it assumes that you do not need to perform any transformations
    on them; the reason is that Symbol is not an AbstractString so it cannot
    be passed to functions that only accept strings (as a benefit comparing
    Symbols is faster than comparing Strings);
  • you are OK with the fact that once Symbol is created the memory it uses up
    will be never reclaimed by the Julia Garbage Collector until the end of the
    session (however, the benefit is that if you have several identical Symbols
    they share the same memory).

Conclusions

Choosing an appropriate type to store your strings is often a quite hard
decision. I hope that after reading this post you have a better overview of
available options and when each of them is appropriate to be used.

It is also recommended to immediately convert the data to an appropriate format
when you read it in. Therefore, e.g. I recommend you to check out the
documentation of the CSV.jl package to learn how to specify what you
want to get when reading the CSV files (the most important keyword arguments
for handling these choices are pool and stringtype).