Advent of Code 2020 Retrospective
adventofcode, 2020, julia
It is Christmas night, and it is the first time this month that I haven’t had to plan my schedule for the evening around a programming puzzle contest. For the last 25 days this month, I participated in Advent of Code 2020, and I managed to collect all 50 stars!
I solved all the puzzles in the Julia programming language, and my solutions are available here.
In this post, I wanted to share why I think you should do Advent of Code and idiomatic solutions to all 25 days in the Julia programming language.
Why Advent of Code
Advent of Code is a lot of fun. I think there’s a few reasons I find it quite enjoyable.
Firstly, there’s the competitive aspect of it.
A single puzzle unlocks every night at midnight Eastern Time, and the time when you submit a solution is recorded. There’s a global leaderboard that highlights the top 100 fastest times but you also have the ability to make a private leaderboard that you can share with anyone you like, and you can use that to track your time and challenge your friends or peers.
For straightforward puzzles, it is a lot of fun to see who can read, grok and type out a bug-free working program the fastest. A bunch of people also upload recordings of their attempts, and it is humbling to see how fast they can whip out a correct solution to a problem.
Secondly, unlike most other competitive programming challenges, the puzzles are mainly designed to be a teaching / learning experience.
Every puzzle has two parts, where the first part introduces a prompt, and requires you to solve it before viewing the second part. The first part tends to set up an idea or check that you are on the right track, and the second part tends to extend the idea or subvert an obvious decision you made in the first part.
Almost all the problems require parsing text input of various formats. In some of the latter puzzles, the puzzle inputs may be designed to hedge against certain naive solutions. Some puzzle inputs will only work with certain data structures or certain algorithms. There are a lot of ah ha
moments when you figure what you should use and when, which makes for a very satisfying solve.
Most problems are based on standard computer science programming concepts, but are never presented as such. Some problems have a mathematics tilt to it, which can make finding those solutions quite rewarding. But also, every problem is designed such that even if you don’t know the theory
behind it you’ll be able to stumble your way into solving it if you persevere. Reading other people’s one liners after you hacked it together is quite enlightening.
And since various programming language communities discuss their solutions in dedicated forums, there tends to be a lot of discussion about the tips and tricks you can use in your favourite programming language to express the problem more elegantly. Even after having used Python and Julia for years now, I still learn new things when I read other people’s solutions.
And finally, that brings me to the community.
The /r/adventofcode subreddit and the Julia Zulip and Slack channel have been a joy to visit every day after solving the puzzles. I’ve particularly enjoyed seeing all the neat visualizations that come out of Advent of Code by the community.
That’s the really neat thing about Advent of Code. The problems are short enough to be solved in less than an hour, the solutions are small enough to be written in just one file, puzzles tend to tackle just one concept or idea, and there is a large enough community around it. Consequently, a lot of people tend to share their solutions, and you get to see a lot of discussion around each puzzle, including a number of different ways to approach the same problem.
If you’ve never heard of Advent of Code, I highly recommend you try it out. Below I’ll be discussing solutions in Julia that were shared on Zulip, Slack and Reddit. This will contain spoilers for all days in Advent of Code 2020.
Solutions
- Report Repair
- Password Philosophy
- Toboggan Trajectory
- Passport Processing
- Binary Boarding
- Custom Customs
- Handy Haversacks
- Handheld Halting
- Encoding Error
- Adapter Array
- Seating System
- Rain Risk
- Shuttle Search
- Docking Data
- Rambunctious Recitation
- Ticket Translation
- Conway Cubes
- Operation Order
- Monster Messages
- Jurassic Jigsaw
- Allergen Assessment
- Crab Combat
- Crab Cups
- Lobby Layout
- Combo Breaker
Day 1: Report Repair
Day 1 of advent of code is basically intended to check that you have a programming language installed, and you know how to use simple features it in (e.g. for
loops).
You can solve the first day with just multiple for
loops. However, a more idiomatic solution can be expressed using the combinations
function from the Combinatorics.jl1:
1 aside: Python has a similar function in the standard library: https://docs.python.org/3/library/itertools.html#itertools.combinations
using Combinatorics
readInput() = sort(parse.(Int, split(strip(read(joinpath(@__DIR__, "./input.txt"), String)))))
expense_report(data, n) = only(prod(items) for items in combinations(data, n) if sum(items) == 2020)
part1(data = readInput()) = expense_report(data, 2)
part2(data = readInput()) = expense_report(data, 3)
In Julia, small functions are usually made into their single line form. Here’s how you would write it if you would like to do it in the multi-line form.
function part1(data = readInput())
expense_report(data, 2)
end
Functions also implicitly return the last expression evaluated as part of the function body, if an explicit return
is not present.
Day 2: Password Philosophy
Day 2 is a simple case of parsing, counting characters in a string and knowing that exactly one
can be expressed using the xor
operation.
The solution below is based on Sukera’s.
readInput() = split(strip(read(joinpath(@__DIR__, "./input.txt"), String)), '\n')
function parseInput(data)
= split.(data, ": ")
d map(d) do (policy,password)
= split(policy, ' ')
rule, letter = parse.(Int, split(rule, '-'))
low, high only(letter), strip(password))
(low, high, end
end
function part1(data = readInput())
count(parseInput(data)) do (low, high, letter, password)
<= count(==(letter), password) <= high
low end
end
function part2(data = readInput())
count(parseInput(data)) do (low, high, letter, password)
== letter) ⊻ (password[high] == letter)
(password[low] end
end
Julia supports an infix operator for xor
: ⊻
.
If a function f
takes another function as the first argument, you can use the f(c) do ... end
block syntax to map over every element in the collection c
and apply the anonymous function defined by the do ... end
block, the result of which is processed by the function f
.
For example, if you would like to do the following:
> is_a(letter) = letter == "a"
julia
> count(is_a, ["a", "b", "c"])
julia1
you can express it like so instead:
> count(["a", "b", "c"]) do letter
julia== "a"
letter end
1
Alternatively, you can pass in an anonymous function as the first argument by using the thin arrow ->
:
> count(letter -> letter == "a", ["a", "b", "c"])
julia1
In Julia, you can use the only
function to get the one and only element in a collection.
> only("h")
julia'h': ASCII/Unicode U+0068 (category Ll: Letter, lowercase)
Day 3: Toboggan Trajectory
A lot of advent of code problems have the puzzle input as text that represents a grid. Day 3 is our first introduction to a grid of trees.
Having a one liner to convert the text input to a Matrix
can be very useful.
readInput() = permutedims(reduce(hcat, collect.(readlines(joinpath(@__DIR__, "./input.txt")))))
Here’s what the input for this particular day looks like:
> A = readInput();
julia
> typeof(A)
juliaMatrix{Char} (alias for Array{Char, 2})
> size(A)
julia323, 31)
(
> xy = findall(==('#'), A);
julia
> using SparseArrays; sparse([p.I[1] for p in xy], [p.I[2] for p in xy], [1 for _ in xy])
julia323×31 SparseMatrixCSC{Int64, Int64} with 2611 stored entries:
⠟⣿⡿⡯⣮⣿⡇
⡗⣿⣿⢿⢝⣽⡇
⡞⡾⣟⢏⣭⢯⡂
⣷⣿⣿⢽⣟⣻⠇
⢺⣯⣗⠽⣟⣿⡂
⡬⡻⡯⡿⣯⣵⡇
⠾⣟⣿⡺⣽⣫⠅
⣿⡿⡏⣯⡈⣻⠅
⢫⢯⣿⣻⡻⡏⡁
⠭⡟⣻⡿⢽⣿⠇
⣝⣽⣷⡑⣟⢽⠇
⢗⣛⣱⣝⠯⡟⡇
⣌⣷⡛⢯⣿⡗⠇
⢷⠿⡍⢶⡯⣟⡅
⡝⣿⡻⣿⣿⢿⡂
⢜⠽⢯⣿⣻⣇⠁
⠹⢿⢻⡯⡟⡛⡅
⣷⣮⣽⣿⡿⡻⡃
⣻⡯⣞⣷⣿⡏⠆
⣮⡗⣟⢫⣵⣿⡆
⣌⣶⣾⣢⣿⣷⡅
⣜⡷⡿⠷⣿⣛⡁
⣶⣽⣮⣾⣏⡋⡅
⣭⣽⣃⣧⣾⣳⡃
⣟⣿⡿⣯⣿⡽⡀
⣼⣪⣗⣯⡖⡿⡃
⣯⢿⡹⣻⣯⣇⡇
⣵⣟⡝⢾⢽⣳⠅
⡿⣯⡿⣻⣿⣿⡃
⣇⣿⣟⣶⣿⣦⡇
⡺⣝⣷⣎⢟⣛⡅
⣻⢏⣯⣟⣎⣓⡅
⡕⣿⣿⣵⣕⢽⡇
⡿⣟⣿⣮⣯⣷⡃
⣟⡃⣇⡻⣿⣯⡇ ⣠⣧⣾⣟⣞⢿⠀
This solution is based on Henrique Ferrolho’s solution.
function solve(trees, slope)
= cld(size(trees, 1), slope.y)
n = range(1, step=slope.y, length=n)
rs = range(1, step=slope.x, length=n)
cs = map(c -> mod1(c, size(trees, 2)), cs)
cs = CartesianIndex.(rs, cs)
idxs count(==('#'), trees[idxs])
end
part1(data = readInput()) = solve(data, (x = 3, y = 1))
part2(data = readInput()) = prod(solve.(Ref(data), [(x=1,y=1),(x=5,y=1),(x=3,y=1),(x=7,y=1),(x=1,y=2)]))
In Julia, you ==(e)
returns a fixed
function which partially applies over the value of e
and accepts one argument which you can use to test equality.
> ==('#')('#')
juliatrue
> ==('#')('.')
juliafalse
Julia has mod1
for 1 based mod, which is useful for indexing in these type of situations. Julia also has ceiling division (cld
) and floor division (fld
) which happen to be handy here.
Julia has support for broadcasting using the f.(c)
syntax, which allows for the element by element application of the method f
on every element in the collection c
, i.e. f(e) for e in c
. This tends to be very handy in Advent of Code.
> c = [1, 2, 3, 4, 5];
julia
> f(x::Int) = x + 1;
julia
> println(f.(c))
julia2, 3, 4, 5, 6] [
Additionally, you can use Ref(data)
to tell Julia that it is a singleton that shouldn’t be broadcast over. Alternatively, you can use (data,)
to get the same behavior.
Day 4: Passport Processing
Since the input has passports separated by an empty line, you can split on "\n\n"
to get each passport into an element of a Vector
.
readInput() = split(read(joinpath(@__DIR__, "./input.txt"), String), "\n\n")
This is what the first 5 passports look like:
> readInput() |> x -> first(x, 3)
julia5-element Vector{SubString{String}}:
"hgt:159cm\npid:561068005 eyr:2025 iyr:2017 cid:139 ecl:blu hcl:#ceb3a1\nbyr:1940"
"iyr:2014\nbyr:1986 pid:960679613 eyr:2025 ecl:hzl"
"cid:211 ecl:blu hcl:#7d3b0c iyr:2011 pid:006632702\nbyr:1982 eyr:2023 hgt:68in"
Julia allows piping the results of one function into another using |>
.
Learning how to use regex well in your programming language of choice can make solutions concise and terse. Check out this solution by Pablo Zubieta:
const fields1 = (r"byr", r"iyr", r"eyr", r"hgt", r"hcl", r"ecl", r"pid")
const fields2 = (
r"byr:(19[2-9][0-9]|200[0-2])\b",
r"iyr:20(1[0-9]|20)\b",
r"eyr:20(2[0-9]|30)\b",
r"hgt:(1([5-8][0-9]|9[0-3])cm|(59|6[0-9]|7[0-6])in)\b",
r"hcl:#[0-9a-f]{6}\b",
r"ecl:(amb|blu|brn|gry|grn|hzl|oth)\b",
r"pid:\d{9}\b"
)
part1(data = readInput()) = count(p -> all(t -> contains(p, t), fields1), data)
part2(data = readInput()) = count(p -> all(t -> contains(p, t), fields2), data)
There were a lot of puzzles this year where I would have been able to parse the input more easily by knowing just a little bit more regex.
Day 5: Binary Boarding
Sometimes having a little insight into what the problem is asking can go a long way. For example, in this puzzle, the seat ID is just a binary representation of the input.
So you can calculate the seat ID using binary shifting or by converting the input to 1
s and 0
s and parsing the input as a binary number directly.
This solution is based on Andrey Oskin’s:
seatid(s) = reduce((x, y) -> (x << 1) | ((y == 'R') | (y == 'B')), s; init = 0)
# OR
seatid(s) = parse(Int, map(c -> c ∈ ('R', 'B') ? '1' : '0', s), base = 2)
part1() = mapreduce(seatid, max, eachline(joinpath(@__DIR__, "./input.txt")))
function part2()
= sort(seatid.(eachline(joinpath(@__DIR__, "./input.txt"))))
seats = first(seats)
prev for seat in seats
- prev == 2) && return prev + 1
(seat = seat
prev end
end
The eachline
function is an alternative to readlines
. It iteratively reads from a stream or IO.
Day 6: Custom Customs
Day 6 introduces set operations with the prompt asking you to identify any
and every
question, which can be expressed using union
and intersect
.
readInput() = split.(split(read(joinpath(@__DIR__, "./input.txt"), String), "\n\n"))
part1(data = readInput()) = sum(q -> length(∪(Set.(q)...)), data)
part2(data = readInput()) = sum(q -> length(∩(Set.(q)...)), data)
In Julia, you can use the unicode symbols of mathematical operations for union
and intersect
of sets, namely ∪
and ∩
respectively. In the Julia REPL or text editors with Julia plugins, you can use the name and tab complete to get the unicode symbol.
> \cup<TAB>
julia
> ∪
juliafunction with 5 methods)
union (generic
> \cap<TAB>
julia
> ∩
juliafunction with 19 methods) intersect (generic
Also, julia has methods on functions like sum
that accept a function as the first argument, which is useful for mapping over every element in a collection.
The ...
operator can be used to splat elements from a collection into arguments of a function.
> f(a, b, c) = @show a, b, c;
julia
> x = [1, 2, 3];
julia
> f(x...);
julia= (1, 2, 3) (a, b, c)
Day 7: Handy Haversacks
Day 7 is the first introduction to graphs this year. While it is possible to find solutions to both parts of this puzzle using recursion, the problem can be well represented as a graph.
This code is based on Ali Hamed Moosavian’s and Andrey Oskin’s solutions and visualizations:
using LightGraphs
using SimpleWeightedGraphs
readInput() = build_graph(split(strip(read(joinpath(@__DIR__, "./input.txt"), String)), '\n'))
function build_graph(data)
= []
edges for line in data
= split(line, " contain ")
outer_bag, inner_bags occursin("no other bags", inner_bags) && continue
for bag in split(inner_bags, ", ")
= parse(Int, first(bag)), strip(bag[3:end], '.')
counter, name e = String(rstrip(outer_bag, 's')), String(rstrip(name, 's')), counter
push!(edges, e)
end
end
= collect(Set(src for (src, _, _) in edges) ∪ Set(dst for (_, dst, _) in edges))
nodes = Dict(n => i for (i,n) in enumerate(nodes))
mapping
= SimpleWeightedDiGraph(length(nodes))
g for (src, dst, counter) in edges
add_edge!(g, mapping[src], mapping[dst], counter)
end
g, mapping, nodesend
Now that you have built a graph, you can find the solution by just traversing the graph.
part1(data = readInput()) = part1(data[1], data[2])
part1(g, mapping) = count(!=(0), bfs_parents(g, mapping["shiny gold bag"], dir = :in)) - 1
function total_bags(g, v)
isempty(neighbors(g, v)) && return 1
1 + sum(neighbors(g, v)) do nb
Int(g.weights[nb, v]) * total_bags(g, nb)
end
end
part2(data = readInput()) = part2(data[1], data[2])
part2(g, mapping) = total_bags(g, mapping["shiny gold bag"]) - 1
Julia allows for multiple dispatch based on the number of arguments and the type of each argument. This lets you define multiple methods of a function.
Day 8: Handheld Halting
Day 8 appears to be a straightforward op code interpreter.
readInput() = strip(read(joinpath(@__DIR__, "./input.txt"), String))
part1(data = readInput()) = boot(split(data, '\n'))
part2(data = readInput()) = corrupt(split(data, '\n'))
function boot(instructions)
= 0, 1, Set{Int}()
acc, i, s while true
∈ s ? break : push!(s, i)
i = split(instructions[i])
inst, n = parse(Int, n)
n == "acc" && ( i += 1; acc += n )
inst == "jmp" && ( i += n )
inst == "nop" && ( i += 1 )
inst end
accend
function corrupt(original_instructions)
for j in 1:length(original_instructions)
= false
boot_loop_detected = 0, 1, Set{Int}()
acc, i, s = copy(original_instructions)
instructions if occursin("jmp", instructions[j])
= replace(instructions[j], "jmp" => "nop")
instructions[j] elseif occursin("nop", instructions[j])
= replace(instructions[j], "nop" => "jmp")
instructions[j] end
while true
∈ s ? ( boot_loop_detected = true; break ) : push!(s, i)
i > length(instructions) && break
i = split(instructions[i])
inst, n = parse(Int, n)
n == "acc" && ( i += 1; acc += n )
inst == "jmp" && ( i += n )
inst == "nop" && ( i += 1 )
inst end
&& return acc
!boot_loop_detected end
end
I believe this problem can also be represented as a graph and solved using memoized depth first search. I would like to re-write this solution using LightGraphs.jl
.
Day 9: Encoding Error
Day 9 was also straightforward.
readInput() = parse.(Int, split(strip(read(joinpath(@__DIR__, "./input.txt"), String)), '\n'))
function check(numbers, n)
for i in numbers, j in numbers
+ j == n && return true
i end
false
end
function part1(numbers = readInput())
= 25
preamble for i in (preamble + 1):length(numbers)
check(numbers[i-preamble:i-1], numbers[i]) && continue
return i, numbers[i]
end
end
function part2(numbers = readInput())
= part1(numbers)
idx, num for i in eachindex(numbers), j in i:lastindex(numbers)
sum(numbers[i:j]) == num && return sum(extrema(numbers[i:j]))
end
end
Julia has a function called extrema
that computes the minimum and maximum element in a single pass.
> extrema([1,2,3,4,5])
julia1, 5) (
Teo ShaoWei’s solution using Combinatorics.jl is also quite elegant.
using Combinatorics
readInput() = parse.(Int, split(strip(read(joinpath(@__DIR__, "./input.txt"), String)), '\n'))
function bad_number(nums, k)
for i in (k + 1):length(nums)
if !any(num1 + num2 == nums[i] for (num1, num2) in combinations(nums[i-k:i-1], 2))
return (i, nums[i])
end
end
end
function rectify(nums, k)
= bad_number(nums, k)
v = j = 1
i while (s = sum(nums[i:j])) != v
< v ? j += 1 : i += 1
s end
return minimum(nums[i:j]) + maximum(nums[i:j])
end
= readInput()
input = bad_number(input, 25)
part1 = rectify(input, 25) part2
Day 10: Adapter Array
Part 2 on this day asks to find the number of distinct ways to arrange the Jolt adapters to connect the charging outlet to the device.
This problem can be formulated as a dynamic programming problem. This is most straightforward to solve using recursion and memoization. Here’s Tom Kwong’s solution:
function readInput()
= parse.(Int, split(strip(read(joinpath(@__DIR__, "./input.txt"), String)), '\n')) |> sort
data vcat(0, data, data[end]+3)
end
part1(data = readInput()) = count(==(1), diff(data)) * count(==(3), diff(data))
function part2(data = readInput())
= length(data)
len = Dict{Int,Int}()
dct function helper(v, i)
haskey(dct, i) && return dct[i]
== len && return 1
i = v[i+1] - v[i] <= 3 ? helper(v, i+1) : 0
n1 = i+2 <= len && v[i+2] - v[i] <= 3 ? helper(v, i+2) : 0
n2 = i+3 <= len && v[i+3] - v[i] <= 3 ? helper(v, i+3) : 0
n3 = n1 + n2 + n3
val = val
dct[i] return val
end
helper(data, 1)
end
One key insight here is that the data doesn’t contain jolt adapters that are only 1 or 3 apart.
> StatsBase.countmap(diff(readInput()))
juliaDict{Int64, Int64} with 2 entries:
3 => 32
1 => 71
Since any adapter that is 3 away can’t be removed, the number of distinct ways is just the product of all the different ways you can choose two adapters from the set of 1 away adapters that are in between the 3 away adapters. Here’s a solution based on Jonnie Diegelman’s that takes advantage of that:
function readInput()
= parse.(Int, split(strip(read(joinpath(@__DIR__, "./input.txt"), String)), '\n')) |> sort
data = vcat(0, data, data[end]+3)
data join(string.(diff(data)))
end
part1(data = readInput()) = count(==('1'), data) * count(==('3'), data)
part2(data = readInput()) = prod(binomial.(length.(split(data, '3', keepempty=false)), 2) .+ 1)
This only works however when the set of 1 away adapters is not longer than 5 elements, which is the case in our puzzle inputs.
Alternatively, because the steps needed are one, two or three, you can calculate all possible steps by using a tribonacci sum. The tribonacci sum gives us all ways to traverse a set of ones, i.e. 11111...
by hopping from 1
to 1
in steps of size 1, 2 or 3.
Thanks to Sukera and Andrey Oskin for their code and insight into solving this puzzle.
function readInput()
= parse.(Int, split(strip(read(joinpath(@__DIR__, "./input.txt"), String)), '\n')) |> sort
data = vcat(0, data, data[end]+3)
data split(join(string.(diff(data))), '3', keepempty = false)
end
function tribonacci(n)
<= 1 && return 1
n == 2 && return 2
n tribonacci(n-1) + tribonacci(n-2) + tribonacci(n-3)
end
part2(data = readInput()) = prod(tribonacci.(length.(data)))
The tribonacci sequence can also be generalized:
const a1 = (19 + 3sqrt(33))^(1/3)
const a2 = (19 - 3sqrt(33))^(1/3)
const b = (586 + 102sqrt(33))^(1/3)
tribonacci(n) = round(Int, (3b * (1/3 * (a1 + a2 + 1))^(n + 1))/(b^2 - 2b + 4))
See the Wikipedia article for more information.
Day 11: Seating System
This is the first tribute in Advent of Code 2020 to John Conway, who passed away earlier this year. John Conway was an English mathematician, most notably famous for the invention of the cellular automation called the Game of Life.
readInput() = permutedims(reduce(hcat, collect.(split(strip(read(joinpath(@__DIR__, "./input.txt"), String)), '\n'))))
part1(data = readInput()) = simulate(data, 0, 1)
part2(data = readInput()) = simulate(data, 1, size(data, 1) * 2)
function simulate(grid, company, sight)
while true
= deepcopy(grid)
old_grid tick(grid, company, sight)
== old_grid && break
grid end
count(==('#'), grid)
end
function tick(grid, company, sight)
= size(grid)
rows, cols = deepcopy(grid)
old_grid for r in 1:rows, c in 1:cols
= adjacent_seats(old_grid, r, c, sight)
A == 'L' && count(==('#'), A) == 0 && ( grid[r, c] = '#' )
grid[r, c] == '#' && count(==('#'), A) >= 4 + company && ( grid[r, c] = 'L' )
grid[r, c] end
end
function adjacent_seats(grid, i, j, sight)
= []
A for direction in CartesianIndex.([(-1,-1), (-1,+1), (+1,-1), (+1, +1), (-1,0), (+1,0), (0,-1), (0,+1)])
= CartesianIndex(i, j) + direction
xy = 1
counter while checkbounds(Bool, grid, xy) && grid[xy] == '.' && counter < sight
+= direction
xy += 1
counter end
checkbounds(Bool, grid, xy) && push!(A, grid[xy])
end
Aend
The key thing to remember when implementing cellular automata is to copy the grid at each tick.
Julia’s CartesianIndex
makes it easier to deal with multi-dimensional indexing. Additionally, I found the checkbounds
function quite handy for this problem.
Day 12: Rain Risk
This was one of my better performances on the Julia leaderboard. Using complex numbers makes it quite straightforward to deal with problems involving rotation.
readInput() = split(strip(read(joinpath(@__DIR__, "./input.txt"), String)), '\n')
function part1(data = readInput())
= map(data) do d
data first(d), parse(Int, d[begin+1:end])
end
= 0 + 0im
current = 1 + 0im
direction for (action, move) in data
if action == 'N' current += move * im
elseif action == 'S' current -= move * im
elseif action == 'E' current += move
elseif action == 'W' current -= move
elseif action == 'F' current += direction * move
elseif action == 'L' direction *= im^(move ÷ 90)
elseif action == 'R' direction *= (-im)^(move ÷ 90)
else error("Unrecognized $action, $move") end
end
abs(current.re) + abs(current.im)
end
function part2(data = readInput())
= map(data) do d
data first(d), parse(Int, d[begin+1:end])
end
= 10 + 1im
waypoint = 0 + 0im
current = 1 + 0im
direction for (action, move) in data
if action == 'N' waypoint += move * im
elseif action == 'S' waypoint -= move * im
elseif action == 'E' waypoint += move
elseif action == 'W' waypoint -= move
elseif action == 'F' current += waypoint * move
elseif action == 'L' waypoint *= im^(move ÷ 90)
elseif action == 'R' waypoint *= (-im)^(move ÷ 90)
else error("Unrecognized $action, $move") end
end
abs(current.re) + abs(current.im)
end
Thanks to Colin Caine for suggesting using if ... elseif ... end
for minor performance improvements. Check out his other solutions for more optimized takes on the problems.
Michael Krabbe Borregaard had some neat visualizations for this day.
Day 13: Shuttle Search
This was one of the harder days for me. I had never heard of Chinese Remainder Theorem (CRT) and apparently that’s what this problem was based on.
Here’s a solution by Micah Halter that uses the CRT
function from Mods.jl package.
using Mods
function readInput()
= readlines(joinpath(@__DIR__, "./input.txt"))
t_str, buses_str = parse(Int, t_str)
time = map(x->x=="x" ? nothing : parse(Int, x), split(buses_str, ','))
buses
time, busesend
function part1(data = readInput())
= data
time, buses = min(map(x->(x*ceil(time / x), x), filter(!isnothing, buses))...)
wait, bus - time) * bus
(wait end
function part2(data = readInput())
= data
_, buses = map(k->Mod{buses[k]}(-(k-1)), filter(k->!isnothing(buses[k]), keys(buses)))
mods CRT(mods...).val
end
Here’s another solution from Doug that finds the cycles using lcm
much more elegantly than how I ended up doing it. The key bit of insight here is that the lcm(previous_bus_schedules, new_bus_schedule)
will be the cycle at which the pattern repeats. Using this, you can break the problem down by iteratively and calculate the next cycle.
function readInput()
= readlines(joinpath(@__DIR__, "./input.txt"))
input = parse(Int, input[1])
n = parse.(Int, replace(split(input[2], ","), "x" => "-1"))
schedule = filter(!=(-1), schedule)
bus_n = (1:length(schedule))[schedule .!= -1] .- 1
bus_dt
bus_n, bus_dt, nend
function part1(data = readInput())
= data
bus_n, bus_dt, n = findmin(bus_n .- n .% bus_n)
min_rem, min_rem_i * min_rem
bus_n[min_rem_i] end
function part2(data = readInput())
= data
bus_n, bus_dt, n = bus_n[1]
inc = 0
n for (i, offset) in zip(bus_n[2:end], bus_dt[2:end])
while (n + offset) % i != 0
+= inc
n end
= lcm(inc, i)
inc end
return n
end
Day 14: Docking Data
This puzzle requires dealing with bit shifting and masking of bits.
Here’s Pablo Zubieta’s solution:
function parse_mask_ops(line)
= split(line, r"\n"; limit = 2, keepempty = false)
mask, list = Tuple{Int,Int}[]
instructions for m in eachmatch(r"mem.(\d+). = (\d+)", list)
= m.captures
address, n push!(instructions, (parse(Int, address), parse(Int, n)))
end
return mask, instructions
end
readInput() = parse_mask_ops.(split(read(joinpath(@__DIR__, "./input.txt"), String), r"mask = "; keepempty = false))
struct Part1 end
struct Part2 end
function write!(::Part1, memory, mask, addr, n)
for (i, bit) in enumerate(mask)
if bit != 'X'
= 1 << (36 - i)
m = (bit == '1') ? (n | m) : (n & ~m)
n end
end
= n
memory[addr] end
function write!(::Part2, memory, mask, addr, n)
= Int8[]
inds for (i, bit) in enumerate(mask)
if bit == '1'
|= (1 << (36 - i))
addr elseif bit == 'X'
push!(inds, i)
end
end
= length(inds)
l for p in 0:(2^l - 1)
for (k, i) in enumerate(inds)
= 1 << (36 - i)
m = (p >> (l - k)) & 1
b = b != 0 ? (addr | m) : (addr & ~m)
addr end
= n
memory[addr] end
end
function solve(p::Union{Part1,Part2}, input)
= Dict{Int,Int}()
memory for (mask, list) in input
for (address, n) in list
write!(p, memory, mask, address, n)
end
end
return sum(values(memory))
end
part1(data = readInput()) = solve(Part1(), data)
part2(data = readInput()) = solve(Part2(), data)
I liked this solution because it was clean and fast:
> @btime part1();
julia392.104 μs (2987 allocations: 251.47 KiB)
> @btime part2();
julia5.218 ms (4426 allocations: 5.96 MiB)
For comparison, these were the times from my original solution:
> @btime part1();
julia2.860 ms (75635 allocations: 4.77 MiB)
> @btime part2();
julia287.836 ms (6234948 allocations: 319.65 MiB)
There are more optimized solutions though, for example here are the timings for the solution posted by Colin Caine:
> @btime part1();
julia25.614 μs (14 allocations: 23.98 KiB)
> @btime part2();
julia3.354 ms (41 allocations: 5.67 MiB)
> @btime part2a();
julia2.865 ms (37 allocations: 5.67 MiB)
Day 15: Rambunctious Recitation
This puzzle seemed to mainly focus on choosing the right data structure for the history
of the memory game. Storing it as a mapping of number to list of indices works well for both parts. A mapping of indices to number will cause the code to be slow, and will make solving part 2 impractical.
Here’s a solution by Sukera:
readInput() = parse.(Int, split(readline(joinpath(@__DIR__, "./input.txt")), ','))
makeArr() = Int[]
function solve(input, goal=2020)
= Dict{Int, Vector{Int}}()
history for (idx, num) in enumerate(input)
= [idx]
history[num] end
= length(input) + 1
turn = 0
num for t in turn:goal-1
= get!(history, num, makeArr())
arr push!(arr, t)
if isone(length(arr))
= 0
num else
= abs(arr[end-1] - arr[end])
num end
end
numend
part1(data = readInput()) = solve(data)
part2(data = readInput()) = solve(data, 30000000)
This video is worth checking out too:
Day 16: Ticket Translation
Calculating which fields were invalid for part 1 was rather straightforward.
function readInput()
= strip(read(joinpath(@__DIR__, "./input.txt"), String))
data = split(data, "\n\n")
rules, your_ticket, nearby_tickets = Dict(map(split(rules, '\n')) do rule
rules = match(r"([\w ]+): (\d+)-(\d+) or (\d+)-(\d+)", rule)
rule, r1start, r1end, r2start, r2end = parse.(Int, [r1start, r1end, r2start, r2end])
r1start, r1end, r2start, r2end => (r1start:r1end, r2start:r2end)
rule end)
= parse.(Int, split(split(your_ticket, '\n')[2], ','))
your_ticket = [parse.(Int, ticket) for ticket in split.(split(nearby_tickets, '\n')[2:end], ',')]
nearby_tickets
rules, your_ticket, nearby_ticketsend
function part1(data = readInput())
= data
rules, your_ticket, nearby_tickets = Int[]
invalid_fields for ticket in nearby_tickets, field in ticket
any([field ∈ rule for rule in Iterators.flatten(values(rules))]) && push!(invalid_fields, field)
!end
sum(invalid_fields)
end
I believe part 2 is better expressed as a graph where you solve the max flow problem to find the maximum matching. However, this is not how I solved it below. I’m hoping to re-solve this problem using LightGraphs.jl.
function part2(data = readInput())
= data
rules, your_ticket, nearby_tickets
= Int[]
invalid_tickets for (i, ticket) in enumerate(nearby_tickets), field in ticket
any([field ∈ rule for rule in Iterators.flatten(values(rules))]) && push!(invalid_tickets, i)
!end
= deleteat!(nearby_tickets, invalid_tickets)
valid_tickets
= ones(Bool, length(first(valid_tickets)), length(rules))
valid for ticket in valid_tickets, (i, field) in enumerate(ticket), (j, rule) in enumerate(rules)
= rule
_, (rule1, rule2) ∈ rule1 || field ∈ rule2) && ( valid[i, j] = false )
!(field end
= [0 for _ in 1:length(rules)]
final = Set{Int}()
accounted_for while length(accounted_for) != length(rules)
for i in 1:length(first(valid_tickets))
= [j for j in 1:length(rules) if valid[i, j] && j ∉ accounted_for]
valid_rules if length(valid_rules) == 1
= only(valid_rules)
final[i] push!(accounted_for, only(valid_rules))
end
end
end
= 1
answer for (interest, k) in enumerate(keys(rules))
startswith(k, "departure") && continue
!for (i, index) in enumerate(final)
== interest && ( answer *= your_ticket[i] )
index end
end
answerend
My learning from this day was that in Julia you can refactor the code with multiple nested for
loops:
for ticket in valid_tickets
for (i, field) in enumerate(ticket)
for (j, rule) in enumerate(rules)
= rule
_, (rule1, rule2) ∈ rule1 || field ∈ rule2) && ( valid[i, j] = false )
!(field end
end
end
into a single line like so:
for ticket in valid_tickets, (i, field) in enumerate(ticket), (j, rule) in enumerate(rules)
= rule
_, (rule1, rule2) ∈ rule1 || field ∈ rule2) && ( valid[i, j] = false )
!(field end
You can even use the variable from the outer loop as the index in the inner loop, like you’d expect. This can help reduce the nesting level of your inner expressions.
Day 17: Conway Cubes
Another homage to John Conway, this time in multiple dimensions.
Here’s Michael Krabbe Borregaard’s succinct solution that takes advantage of CartesianIndices
:
function parsefield(lines, n, dims)
= falses(fill(n, dims)...)
field for (i,line) in enumerate(lines), (j, char) in enumerate(line)
== '#' && (field[i + n÷2, j + n÷2, fill(n÷2, dims-2)...] = true)
char end
fieldend
function startup(lines, rounds, dims)
= parsefield(lines, 2*(length(lines) + rounds + 2), dims)
field = copy(field)
newfield for n in 1:rounds
for ind in CartesianIndices(field)[fill(2+rounds-n:size(field, 1)-rounds-1+n, dims)...]
= sum(field[((-1:1) .+ i for i in Tuple(ind))...]) - field[ind]
counter = field[ind] ? counter in (2,3) : counter == 3
newfield[ind] end
.= newfield
field end
sum(field)
end
readInput() = readlines(joinpath(@__DIR__, "./input.txt"))
part1(data = readInput()) = startup(data, 6, 3)
part2(data = readInput()) = startup(data, 6, 4)
Cellular automata puzzles are always fun, and make for some neat visualizations. Here are a couple of multi dimensional visualizations by Tom Kwong:
Day 18: Operation Order
The intended way to solve such problems is by implementing the Shunting-yard algorithm.
However, you can hack the operator precedence in your programming language of choice and take advantage of the built in parser.
Here is Doug’s that managed to get him a spot on the global leaderboard.
readInput() = readlines(joinpath(@__DIR__, "./input.txt"))
⨦(a,b) = a * b # define "multiplication" with same precedence as "+"
part1(data = readInput()) = sum(l -> eval(Meta.parse(replace(l, "*" => "⨦"))), data)
⨱(a,b) = a + b # define "addition" with precedence of "*"
part2(data = readInput()) = sum(l -> eval(Meta.parse(replace(replace(l, "*" => "⨦"), "+" => "⨱"))), data)
The key is to find the right operators in your programming language that have the precedence required as per the prompt.
Day 19: Monster Messages
This was another hard day for me. First, I tried to implement a recursive algorithm. After failing to figure this out, I picked it up again on the next day with a clean slate, and I tried to build a regex that would match various messages. This worked for part 1 but I kept running out of memory for part 2. Finally, after changing some of the rules hard-coding them by hand I was able to solve part 2.
Specifically, I hard coded rules "8"
and "11"
to the following.
"8"] = "(42)+"
rules["11"] = "42 31 | 42 ( 42 31 | 42 ( 42 31 | 42 ( 42 31 | 42 ( 42 31 ) 31 ) 31 ) 31 ) 31" rules[
This limits the depth of rule "11"
. One more step however and I was getting PCRE memory errors.
Here’s a solution by Doug that does the same thing elegantly and programmatically.
function readInput()
= split.(split(strip(read(joinpath(@__DIR__, "./input.txt"), String)), "\n\n"), '\n')
rules, messages = Dict(map(rules) do line
rules = match(r"^(\d+): (\"(\w+)\"|([^|]+)|(.+))$", line)
m String(m[1]) => m[3] isa Nothing ? (m[4] isa Nothing ? "(?:$(m[5]))" : String(m[4])) : String(m[3])
end)
rules, messagesend
function resolve(rule, rules)
= [nsi.match for nsi = eachmatch(r"\b\d+\b", rule)]
ns = [Regex("\\b$nsi\\b") for nsi = ns]
n_res = [resolve(rules[nsi], rules) for nsi=ns]
n_rep replace(reduce(replace, n_res .=> n_rep, init = rule), " " => "")
end
function part1(data = readInput())
= data
rules, messages count(contains(Regex("^$(resolve(rules["0"], rules))\$")), messages)
end
function part2(data = readInput())
= data
rules, messages = resolve(rules["42"], rules)
rule42 = resolve(rules["31"], rules)
rule31 count(contains(Regex("^$rule42+($rule42(?1)?$rule31)\$")), messages)
end
As noted by /u/furiousleep on /r/adventofcode (along with other valuable feedback), the intended solution for this would use the CYK algorithm. I would like to rewrite my solution using this algorithm instead.
Day 20: Jurassic Jigsaw
This puzzle was really fun to figure out but also tedious to type out everything that you needed to type out. While I was able to solve the problem, I hard-coded many things in my solution. My code doesn’t even work for the test cases. This is just one of those puzzles that is easier to solve on paper than to write an actual working implementation.
Here’s a working solution by Alisdair Sullivan:
readInput() = map(t -> split(t, "\n"), split(strip(read(joinpath(@__DIR__, "./input.txt"), String)), "\n\n"))
mutable struct Tile
::Int
id::Array{Char, 2}
rawend
function copy(tile::Tile)
new = copy(tile.raw)
return Tile(tile.id, new)
end
= Tile(1, ['a' 'b' 'c'; 'd' 'e' 'f'])
t
function size(tile::Tile)
return Base.size(tile.raw)
end
function top(tile::Tile)
= size(tile)
(y, x) return tile.raw[range(1, length=x, step=y)]
end
function right(tile::Tile)
= size(tile)
(y, x) return tile.raw[range((x * y) - y + 1, length=y, step=1)]
end
function bottom(tile::Tile)
= size(tile)
(y, x) return tile.raw[range(y, length=x, step=y)]
end
function left(tile::Tile)
= size(tile)
(y, x) return tile.raw[range(1, length=y, step=1)]
end
function flip!(tile)
new = tile.raw[:,end:-1:1]
= new
tile.raw end
function rotate!(tile)
new = rotr90(tile.raw)
= new
tile.raw end
function parse_tiles(tiles)
return map(tile -> parse_tile(tile), tiles)
end
function parse_tile(tile)
= match(r"(\d+)", first(tile))
m = parse(Int, m.captures[1])
id = reduce(vcat, permutedims.(collect.(tile[2:end])))
raw return Tile(id, raw)
end
function find_edges(tile)
= []
edges push!(edges, top(tile))
push!(edges, right(tile))
push!(edges, bottom(tile))
push!(edges, left(tile))
push!(edges, reverse(top(tile)))
push!(edges, reverse(right(tile)))
push!(edges, reverse(bottom(tile)))
push!(edges, reverse(left(tile)))
return edges
end
function orient!(tile, matches)
for flipped in (false, true), i in 1:4
matches(tile) && return
&& flip!(tile)
flipped && matches(tile) && return
flipped && flip!(tile)
flipped rotate!(tile)
end
end
function find_corners(tiles)
= []
corners = count_edges(tiles)
ec
for tile in tiles
= sum([length(ec[top(tile)]), length(ec[bottom(tile)]), length(ec[left(tile)]), length(ec[right(tile)])])
c == 6 && push!(corners, tile)
c end
return corners
end
function count_edges(tiles)
= Dict{Vector{Char}, Vector{Int}}()
acc for tile in tiles
= find_edges(tile)
edges for edge in edges
haskey(acc, edge) ? acc[edge] = append!(acc[edge], tile.id) : acc[edge] = [tile.id]
end
end
return acc
end
function find_origin(tiles)
= count_edges(tiles)
edges = first(find_corners(tiles))
corner for _ in 1:4
= edges[top(corner)]
t = edges[left(corner)]
l length(t) == 1 && length(l) == 1 && return corner
rotate!(corner)
end
end
function trim(tile)
= tile.raw
raw = size(tile)
(x, y) return raw[2:(x - 1),2:(y - 1)]
end
function assemble(tiles)
= find_origin(deepcopy(tiles))
origin = count_edges(deepcopy(tiles))
edges
= []
rows = [origin]
row
= origin
rowstart = origin
current
while !isnothing(current)
= edges[right(current)]
match if length(match) == 2
= only(filter(id -> id != current.id, match))
next_id = findfirst(t -> t.id == next_id, tiles)
next_idx = tiles[next_idx]
next = (t) -> left(t) == right(current)
matcher orient!(next, matcher)
push!(row, next)
= next
current else
push!(rows, row)
= edges[bottom(rowstart)]
match if length(match) == 2
= only(filter(id -> id != rowstart.id, match))
next_id = findfirst(t -> t.id == next_id, tiles)
next_idx = tiles[next_idx]
next orient!(next, (t) -> top(t) == bottom(rowstart))
= next
current = current
rowstart = [rowstart]
row else
= nothing
current end
end
end
return vcat(map(row -> hcat(map(tile -> trim(tile), row)...), rows)...)
end
function find_seamonsters(assembled)
# seamonster pattern
= [
indices CartesianIndex(19, 1),
CartesianIndex(1, 2),
CartesianIndex(6, 2),
CartesianIndex(7, 2),
CartesianIndex(12, 2),
CartesianIndex(13, 2),
CartesianIndex(18, 2),
CartesianIndex(19, 2),
CartesianIndex(20, 2),
CartesianIndex(2, 3),
CartesianIndex(5, 3),
CartesianIndex(8, 3),
CartesianIndex(11, 3),
CartesianIndex(14, 3),
CartesianIndex(17, 3)
]
= Base.size(assembled) .- (20, 3)
(xr, yr)
= []
counts
for flipped in (false, true), i in 1:4
= assembled[end:-1:1,:] : nothing
flipped ? assembled = 0
count = CartesianIndex(0,0)
idx for j in 1:yr
= Tuple(idx)
(x, y) = CartesianIndex(0, y + 1)
idx for i in 1:xr
+= CartesianIndex(1, 0)
idx all(i -> assembled[idx + i] == '#', indices) ? count += 1 : nothing
end
end
push!(counts, count)
= assembled[end:-1:1,:] : nothing
flipped ? assembled = rotr90(assembled)
assembled end
return counts
end
function check(puzzle)
= 0
acc for i in eachindex(puzzle)
== '#' ? acc += 1 : nothing
puzzle[i] end
return acc
end
function part1(tiles = readInput())
= parse_tiles(tiles)
tiles = find_corners(tiles)
corners prod(map(tile -> tile.id, corners))
end
function part2(tiles = readInput())
= parse_tiles(tiles)
tiles = find_corners(tiles)
corners
= assemble(tiles)
assembled = find_seamonsters(assembled)
counts
check(assembled) - (maximum(counts) * 15)
end
The key functions I found that others were using were rotl90
, rotr90
and rot180
from the Julia standard library, like in Pablo Zubieta’s solution.
Mark Kittisopikul also used imfilter
from ImageFiltering.jl to find the monsters, which I thought was pretty neat.
Day 21: Allergen Assessment
This puzzle is another graph problem that can be solved quite elegantly using maximum flow algorithms to find the maximum matching. Here’s the solution of the test case using LightGraphs.jl and LightGraphFlows.jl, as well as a visualization using GraphRecipes.jl and Plots.jl:
using LightGraphs
using LightGraphsFlows
using GraphRecipes, Plots
readInput() = strip(read(joinpath(@__DIR__, "./input.txt"), String))
function m()
= split(strip(readInput()), '\n')
data = map(data) do line
data = match(r"^(.*) \(contains (.*)\)$", line).captures
ingredients, allergens String.(split(ingredients)), String.(split(allergens, ", "))
end
= Set{String}(), Set{String}()
all_ingredients, all_allergens for (ingredients, allergens) in data
= all_ingredients ∪ Set(ingredients)
all_ingredients = all_allergens ∪ Set(allergens)
all_allergens end
= collect(all_ingredients), collect(all_allergens)
all_ingredients, all_allergens
= Dict()
d for (ingredients, allergens) in data, allergen in allergens
∉ keys(d) && ( d[allergen] = Set(ingredients) )
allergen = d[allergen] ∩ Set(ingredients)
d[allergen] end
= length(all_ingredients) + length(all_allergens) + 2
MAX_NODES = SimpleDiGraph(MAX_NODES)
g for i in all_ingredients
= 1 + findfirst(==(i), all_ingredients)
i add_edge!(g, 1, i)
end
for a in all_allergens
= length(all_ingredients) + 1 + findfirst(==(a), all_allergens)
a add_edge!(g, a, MAX_NODES)
end
for (allergen, ingredients) in d, ingredient in ingredients
= 1 + findfirst(==(ingredient), all_ingredients)
i = 1 + length(all_ingredients) + findfirst(==(allergen), all_allergens)
a add_edge!(g, i, a)
end
graphplot(g,
= 1:MAX_NODES,
names = vcat([[1]; [2 for _ in 1:length(all_ingredients)]; [3 for _ in 1:length(all_allergens)]; [4]]),
x = vcat(
y
[1];
[- length(all_ingredients) ÷ 2 for i in 1:length(all_ingredients)];
[i - length(all_allergens) ÷ 2 for a in 1:length(all_allergens)];
[a 1]
[
]
),= vcat([
markercolor "white"];
[colorant"blue" for _ in 1:length(all_ingredients)];
[colorant"green" for _ in 1:length(all_allergens)];
[colorant"white"]
[colorant
]),= 1)
markersize
= maximum_flow(g, 1, MAX_NODES)
_, F
= all_ingredients[[i - 1 for i in 1:MAX_NODES if count(==(0), F[i, :]) == MAX_NODES]]
no_allergen_ingredients
no_allergen_ingredientssum(map(data) do (ingredients, _)
count(i ∈ ingredients for i in no_allergen_ingredients)
end)
end
Here are a couple of visualizations, the test data visualized as a graph is on the left and the maximum flow solution on the right.
Day 22: Crab Combat
This puzzle was mostly straightforward too. The key here seems to be to implement the stopping conditions correctly, and take advantage of the stack based behavior of recursion in most programming languages.
Here’s a solution by Henrique Ferrolho’s.
function play(p₁, p₂, part₂)
= Set{Tuple{Array,Array}}([])
seen
while !any(isempty.((p₁, p₂)))
∈ seen && return 1, p₁ # infinite-game-prevention rule
(p₁, p₂) push!(seen, copy.((p₁, p₂)))
= popfirst!.((p₁, p₂)) # both players draw their top card
c₁, c₂
if part₂ && all(length.((p₁, p₂)) .>= (c₁, c₂))
= play(copy.((p₁[1:c₁], p₂[1:c₂]))..., part₂)
w, _ elseif c₁ > c₂ w = 1
elseif c₂ > c₁ w = 2
else @error "Unexpected case: c₁ == c₂" end
== 1 && push!(p₁, c₁, c₂) # player 1 wins
w == 2 && push!(p₂, c₂, c₁) # player 2 wins
w end
isempty(p₁) ? (1, p₁) : (2, p₂)
!end
function day22()
= split(read(joinpath(@__DIR__, "./input.txt"), String), "\n\n") .|>
p₁, p₂ -> parse.(Int, split(x, '\n', keepempty=false)[2:end])
x
= map([false, true]) do part₂
result₁, result₂ = play(copy.((p₁, p₂))..., part₂)
_, deck = length(deck):-1:1
multipliers sum(deck .* multipliers)
end
result₁, result₂end
part1() = day22()[1]
part2() = day22()[2]
Julia allows using unicode symbols as part of variable names which can make for some pretty looking code.
Day 23: Crab Cups
This puzzle took me a while to figure out. I spent way too long looking for patterns in each state. Thanks to some helpful tips from fellow Julia advent-of-coders, I re-wrote it from scratch using a Linked List.
The key idea is here to manage the ordering in a separate data structure. Using a linked list is just one way to handle this.
Here’s a solution by Nicolas Viennot based on exchanging ideas with Teo ShaoWei that manages to do that quite elegantly using another array to manage indices:
readInput() = parse.(Int32, collect(strip(read(joinpath(@__DIR__, "./input.txt"), String))))
function peek(next, at, n; result=similar(next,n))
for i in 1:n
= next[at]
result[i] = next[at]
at end
resultend
function run(cups, steps=1)
= length(cups)
N = similar(cups, 3)
prealloc
= similar(cups)
next for i in 1:N
= cups[mod1(i+1,N)]
next[cups[i]] end
= cups[1]
current for i in 1:steps
= peek(next, current, 3, result=prealloc)
pickups
= mod1(current-1, N)
dst while dst in pickups
= mod1(dst-1, N)
dst end
= next[pickups[end]]
next[current] end]] = next[dst]
next[pickups[= pickups[1]
next[dst] = next[current]
current end
return next
end
part1(cups = readInput()) = join(peek(run(cups, 100), 1, 8))
part2(cups = readInput()) = prod(peek(run(vcat(cups, 10:1_000_000), 10_000_000), 1, 2))
Day 24: Lobby Layout
In another tribute to John Conway, now you must model hexagon grids.
Having never worked with hexagon grid before, I reached for complex numbers again, which turned out to be a bad idea. I was indexing a dictionary with the real and imaginary components of the complex number which were floating point numbers. This caused all sorts of indexing problems due to rounding issues.
I could have sorted this out by using only integer values or by using the coordinate system described in this resource: https://www.redblobgames.com/grids/hexagons/#coordinates-cube.
Here’s another elegant solution by Nicolas Viennot.
using OffsetArrays
parse_path(line) = getproperty.(eachmatch(r"(e|se|sw|w|nw|ne)", line), :match)
readInput() = parse_path.(readlines(joinpath(@__DIR__, "./input.txt")))
const hex_dirs = Dict(k => CartesianIndex(v) for (k,v) in [
"e" => ( 1, 0),
"se" => ( 1, -1),
"sw" => ( 0, -1),
"w" => (-1, 0),
"nw" => (-1, 1),
"ne" => ( 0, 1)
])
const N = 200
get_destination(path) = mapreduce(x->hex_dirs[x], +, path)
function init_tiles(paths)
= OffsetArray(falses(N,N),-N÷2-1,-N÷2-1)
A foreach(dst -> A[dst] ⊻= 1, get_destination.(paths))
Aend
part1(data = readInput()) = count(init_tiles(data))
function get_neighbor_counts(A)
= OffsetArray(zeros(Int, N-2,N-2),-N÷2,-N÷2)
C for i in CartesianIndices(C)
= sum(A[i+dir] for dir in values(hex_dirs))
C[i] end
Cend
function flip_tiles!(A, steps=1)
for step in 1:steps
= get_neighbor_counts(A)
C for i in CartesianIndices(C)
if (A[i] && C[i] ∉ (1,2)) || (!A[i] && C[i] == 2)
⊻= 1
A[i] end
end
end
Aend
part2(data = readInput()) = count(flip_tiles!(init_tiles(data), 100))
And another neat visualization by Tom Kwong:
Day 25: Combo Breaker
And finally, for the last day, it is a cryptography based puzzle.
The puzzle’s key idea here is based on the Diffie-Hellman key exchange.
Julia also has a function called powermod
in the standard library, which can be used for this. Here’s a solution by Nicolas Viennot:
readInput() = parse.(Int, split(strip(read(joinpath(@__DIR__, "./input.txt"), String)), '\n'))
function part1(data = readInput())
= 20201227
N e = 7
= data
card_public_key, door_public_key = findfirst(i -> powermod(e, i, N) == card_public_key, 1:N)
privkey1 = powermod(door_public_key, privkey1, N)
solution end
If you’ve made it all this way, part 2 of day 25 should be a cinch 😉.
Final words
Thanks to everyone in the Julia community who participated on Zulip, Slack and Reddit. I learnt a lot by reading your solutions and discussing with you all.
Thanks to the mods on /r/adventofcode to making it such a vibrant community to frequent.
And finally, thanks to Eric Wastl for making such a fun event.
Happy holidays everyone!
Reuse
Citation
@online{krishnamurthy2020,
author = {Krishnamurthy, Dheepak},
title = {Advent of {Code} 2020 {Retrospective}},
date = {2020-12-25},
url = {https://kdheepak.com/blog/advent-of-code-2020-retrospective/},
langid = {en}
}