I’ve been playing Advent of Code this year. I started writing solutions in R, but soon realized I was better served by a more general purpose language. Julia took its place.

Day 1

Sum the discrepencies between two sorted lists.

day1(x) = sum(abs(sort(x) - sort(y)))

Day 2

Find the number of rows that are either increasing or decreasing and for which increments are at least 1 but no more than 3.

day2(reports) = sum(map(reports) do r
    δ = r[2:end] .- r[1:end-1]
    Δ = abs.(δ)
    (all(δ .>= 0) | all(δ .<= 0)) & all(Δ .>= 1) & all(Δ .<= 3)
end)

For part two, we allow one of the items in each row to be ignored.

day2_2(reports) = sum(map(reports) do r
    mapreduce(|, 1:length(r)) do damped
    	rm = r[sparsevec([damped], [true], length(r))]
    	δ = rm[2:end] .- rm[1:end-1]
      Δ = abs.(δ)
    	(all(δ .>= 0) | all(δ .<= 0)) & all(Δ .>= 1) & all(Δ .<= 3)
    end
end)

Day 3

Find every instance of the pattern mul(A,B) where A and B are positive integers and return the sum of A*B.

day3(s) = sum(prod(parse.(Int, a)) for a in eachmatch(r"mul\((\d+),(\d+)\)", s))

For part 2, ignore patterns after the string don't() and before the string do().

day3_2(s) = foldl(eachmatch(r"do\(\)|don't\(\)|mul\((\d+),(\d+)\)", s), init=0=>true) do (sofar, state), a
    if a.match == "do()"
        sofar=>true
    elseif a.match == "don't()"
        sofar=>false
    elseif state
        (sofar + prod(parse.(Int, a)))=>state
    else
        sofar=>state
    end
end

Day 4

Find the number of times the string “XMAS” can be found in a given word search. We can parse the input with a function that seems useful for a lot of these problems:

function ascii_grid(lines)
    chars = Vector{Char}[Char.(codeunits(l)) for l in lines]
    permutedims(reduce(hcat, chars))
end

Once the input is read into the matrix img, we can continue with part 1

function day4(img)
    sum(
        try
            "XMAS" == join([img[i + a * dx, j + a * dy] for a in 0:3])
        catch
            false
        end
        for i in 1:size(img, 1) for j in 1:size(img, 2)
        for dx in [-1, 0, 1] for dy in [-1, 0, 1]
    )
end

For part two, find the number of times two “MAS” strings meet in an X shape.

function day4_2(img)
    sum(
        any("MAS" == join([img[i + d * a, j + d * a] for a in -1:1])
            for d in (-1,1)) &&
        any("MAS" == join([img[i + d * a, j - d * a] for a in -1:1]) for d in (-1,1))
        for i in 2:(size(img, 1)-1) for j in 2:(size(img, 2) - 1)
    )
end

Day 5

Find the subset of sequences that obey a set of “comes before” rules. Sum their middle elements. For part two, rearrange the the disqualified sequences so that they no longer break the ordering rules and sum the rearrangements’ middle elements as well. Here I return the solutions to both parts at once.

function day5(pred_rules, seqs)
  h = Set(pred_rules)
  sorted = map(seqs) do s
      sort(s; lt=(a,b)->(a=>b)∈h) == s
  end
  mask = sorted .== seqs
  map([sorted[mask], sorted[.~mask]]) do sub_sorted
    sum([s[ceil(Int, length(s) / 2)] for s in sub_sorted])
  end
end

Day 6

Collect the coordinates visited by robot that moves forward until it hits a wall, then rotates right. The state is given by an ascii grid of characters.

function day6(grid)
    map = Set(collect.(Tuple.(findall(x->x=='#', grid))))
    pos = collect(Tuple(findfirst(x->x=='^', grid)))
    dims = shape(grid)
    dir = [-1, 0]
    encountered = Set([pos])
    while true
        while pos + dir  map
            pos = pos .+ dir
            if any(pos .<= 0) || any(pos .> dims)
                return encountered
            else
                push!(encountered, pos)
            end
        end
        dir = [0 1; -1 0] * dir
    end
end

Day 7

Check whether it’s possible to get a given result value by inserting some sequence of * and + operators in between a given list of arguments. Sum the result values for which this is possible. Part 2 adds a digit concatenation operator.

combine(a,b) = (a * 10^(1 + trunc(Int, log10(b)))) + b

ops = FunctionWrapper{Int,Tuple{Int,Int}}[+, *, combine]

function day7(eqs)
    mask = [
        any(result == foldl(zip(chosen, args[2:end]), init=args[1]) do x, (f, y)
            f(x,y)
        end for chosen in Iterators.product(fill(ops, length(args) - 1)...))
    for (args, result) in eqs]
    sum(last.(eqs[mask]))
end

Day 8

… I’ve realized that abstract descriptions of problems are increasingly going to be impossible to provide. Click the links to read the true problem descriptions.

function day8(img)
    s = Set{CartesianIndex{2}}()
    l = DefaultDict{Char, Vector{CartesianIndex{2}}}(Vector{CartesianIndex{2}})
    for ix in CartesianIndices(img)
        if img[ix] != '.'
            push!(l[img[ix]], ix)
        end
    end
    for ixs in values(l)
        for (i,j) in Iterators.product(ixs, ixs)
            if i == j continue end
            dx = j - i
            for a in 1:size(img, 1)
                anode = i + a * dx
                if checkbounds(Bool, img, anode)
                    push!(s, anode)
                else
                    break
                end
            end
        end
    end
    s
end

Day 9

Part 1:

function day9(s)
    offset = 0
    spaces = Pair{Int,Int}[]
    files = SortedDict{Int, Pair{Int, Int}}()
    for (i, c) in enumerate(parse.(Int, collect(s)))
        if i % 2 == 0
            if c > 0 push!(spaces, offset=>c) end
        else
            files[offset] = div(i - 1, 2)=>c
        end
        offset += c
    end
    reverse!(spaces)
    while !isempty(spaces) && !isempty(files)
        (old_offset, (id, f_amt)) = poplast!(files)
        (offset, s_amt) = pop!(spaces)
        if old_offset <= offset return files end
        stored_amt = min(f_amt, s_amt)
        files[offset] = id=>stored_amt
        f_resid = f_amt - stored_amt
        if f_resid > 0
            files[old_offset] = id=>f_resid
        end
        s_resid = s_amt - stored_amt
        if s_resid > 0
            push!(spaces, (offset + stored_amt)=>s_resid)
        end
    end
    files
end

checksum(a) = sum(sum(id * (pos:(pos + amt - 1))) for (pos, (id, amt)) in a)

Part 2 is much the same, but we use a SortedDict for spaces instead of a vector.

new_files = copy(files)
while !isempty(spaces) && !isempty(files)
  (old_offset, (id, f_amt)) = poplast!(files)
  spc = findfirst(s_amt->s_amt >= f_amt, spaces)
  if isnothing(spc) || old_offset <= first(spc) continue end
  (offset, s_amt) = spc
  delete!(spaces, offset)
  stored_amt = min(f_amt, s_amt)
  delete!(new_files, old_offset)
  new_files[offset] = id=>stored_amt
  s_resid = s_amt - stored_amt
  if s_resid > 0
    spaces[offset + stored_amt] = s_resid
  end
end

Day 10

The code below returns the results for both parts.

function advent10(a)
    ids = LinearIndices(a)
    origins = Int[]
    targets = Int[]
    g = SimpleDiGraph(length(a))
    for ix in CartesianIndices(a)
        if a[ix] == 0 push!(origins, ids[ix]) end
        if a[ix] == 9 push!(targets, ids[ix]) end
        for d in [CartesianIndex(1, 0), CartesianIndex(0, 1)]
            for b in [1, -1]
                ix2 = ix + b * d
                if checkbounds(Bool, a, ix2) && a[ix2] - a[ix] == 1
                    add_edge!(g, ids[ix], ids[ix2])
                end
            end
        end
    end
    dists = (adjacency_matrix(g)^9)[origins, targets]
    (sum(dists .> 0), sum(dists))
end

Day 11

I added another generally useful utility function here:

function weighted_counter(a)
    c = counter(eltype(a).parameters[1])
    for (k, v) in a
        inc!(c, k, v)
    end
    c
end

Using this function, parts 1 and 2 are the same:

function split_digits(a)
	n = trunc(Int, log10(a)) + 1
	if n % 2 == 1 return nothing end
	half = 10^(div(n, 2))
	part1 = div(a, half)
	part2 = a % half
	[part1, part2]
end

function blink(a)
	if a == 0 return [1] end
	s = split_digits(a)
	isnothing(s) ? [a * 2024] : s
end

function blink_n(raw_a, n)
	a = counter(raw_a)
	for _ in 1:n
    a = weighted_counter(k2=>v for (k,v) in a for k2 in blink(k))
	end
	sum(values(a))
end

Day 12

In part 1, we calculate the perimeter of each same-character region in an ascii grid.

function same_along(a, ix, d)
	ix2 = ix + d
	checkbounds(Bool, a, ix2) && a[ix] == a[ix2]
end
function advent12(a)
	ids = LinearIndices(a)
	ds = IntDisjointSets(length(ids))
	neighbors = zeros(Int, length(ids))
	for ix in CartesianIndices(a)
		for d in [CartesianIndex(1, 0), CartesianIndex(0, 1)]
			for s in [1, -1]
				ix2 = ix + s * d
				if checkbounds(Bool, a, ix2) && a[ix] == a[ix2]
					union!(ds, ids[ix], ids[ix2])
					neighbors[ids[ix]] += 1
				end
			end
		end
	end
	roots = find_root!.(Ref(ds), ids)
	non_edges = weighted_counter(zip(roots, neighbors))
	areas = counter(roots)
	sum(areas[k] * (4 * areas[k] - non_edges[k]) for (k,v) in areas)
end

In part 2, we count the number of corners of each region instead.

rot(d) = CartesianIndex(([0 1; -1 0] * collect(Tuple(d)))...)

function advent12_2(a)
	ids = LinearIndices(a)
	ds = IntDisjointSets(length(ids))
	corners = zeros(Int, length(ids))
	for ix in CartesianIndices(a)
		for d in [CartesianIndex(1, 0), CartesianIndex(0, 1)]
			for s in [1, -1]
				if same_along(a, ix, d * s)
					union!(ds, ids[ix], ids[ix + s * d])
					rotated = rot(d*s)
					diag = rotated + (s * d)
					if same_along(a, ix, rotated) && !same_along(a, ix, diag)
						corners[ids[ix]] += 1
					end
				elseif !same_along(a, ix, rot(d * s))
					corners[ids[ix]] += 1
				end
			end
		end
	end
	roots = find_root!.(Ref(ds), ids)
	corner_sums = weighted_counter(zip(roots, corners))
	areas = counter(roots)
	sum(v * corner_sums[k] for (k,v) in areas)
end

Day 13

This problem was to find positive integer vectors $v$ to minimize $(3, 1)^Tv$ such that $Av = p$ for various values of $A$ and $p$. My initial idea was to use an ILP solver.

function advent13(itr)
	s = Iterators.Stateful(itr)
	v = Variable(2, Positive(), IntVar)
	total = 0.0
	while true
		a = parse.(Float64, collect(match(r"X\+(\d+), Y\+(\d+)", popfirst!(s))))
		b = parse.(Float64, collect(match(r"X\+(\d+), Y\+(\d+)", popfirst!(s))))
		ab = hcat(a, b)
		prize = parse.(Float64, collect(match(r"X=(\d+), Y=(\d+)", popfirst!(s))))
		problem = minimize(dot([3, 1], v), [ab * v == prize])
		solve!(problem, HiGHS.Optimizer; silent=true)
		if problem.status == MathOptInterface.OPTIMAL
			total += problem.optval
		end
		if isempty(s)
			return Int(total)
		end
		popfirst!(s)
	end
end

But the answer it provided for part 2 of the problem doesn’t match what Advent of Code was looking for. After further investigation, all the input matrices $A$ were non-singular, so I didn’t need to worry about the optimization part at all. I could just solve $A^{-1}p$ and check if the result was an integer.

result = ab \ prize
rresult = round.(result)
if all(abs.(result - rresult) .< 1e-4)
  total += dot([3,1], result)
end

This ended up giving the answer Advent of Code expected.

Day 14

This task is just simulating a dynamical system forward in time.

function advent14(ls, dims)
	starts = Vector{Int}[]
	velocities = Vector{Int}[]
	for l in ls
		d = parse.(Int, match(r"p=(\d+),(\d+) v=(-?\d+),(-?\d+)", l))
		push!(starts, d[1:2])
		push!(velocities, d[3:4])
	end
	S = stack(starts, dims=1)
	v = stack(velocities, dims=1)
	end_pos = mod.(S .+ T .* v, reshape(dims, (1,2)))
	half = reshape((dims .- 1) ./ 2, (1,2))
	mids = any(end_pos .== half, dims=2)[:, 1]
	prod(values(counter(eachrow(end_pos[.!mids,:] .> half))))
end

Day 15

function advent15(g)
	start = findfirst(g .== '@')
	for instr in instrs
		if instr == '<'
			start = shove!(g, start, CartesianIndex(0, -1))
		elseif instr == '>'
			start = shove!(g, start, CartesianIndex(0, 1))
		elseif instr == '^'
			start = shove!(g, start, CartesianIndex(-1, 0))
		elseif instr == 'v'
			start = shove!(g, start, CartesianIndex(1, 0))
		end
	end
	sum((ix[1] - 1)*100 + ix[2] - 1 for ix in findall(g .== 'O'))
end

function shove!(g, start, dir)
	target = start + dir
	if g[target] == '#'
		return start
	elseif g[target] == 'O'
		next = shove!(g, target, dir)
		if next == target return start end
	end
	g[target] = g[start]
	g[start] = '.'
	target
end

Day 16 This is variation of shortest path finding on a graph, except that transitioning from moving horizontally to moving vertically introduces an extra cost of 1000 steps. I’ll add the following general purpose utility to translate between vertex labels and integers.

Base.@kwdef mutable struct IntMapper{K}
    dict::Dict{K, Int} = Dict{K, Int}()
    counter::Int = 0
end

function code_for(m::IntMapper{K}, a::K) where {K}
    if !haskey(m.dict, a)
        m.counter += 1
        m.dict[a] = m.counter
    end
    m.dict[a]
end

To solve the problem, just create two vertices per grid position: one with a horizontal orientation and one with a vertical one. The edges between these two copies have weight 1000.

function advent16(g)
	entries = []
	m = IntMapper{Pair{CartesianIndex{2}, Bool}}()
	for ix in CartesianIndices(g)
		if g[ix] == '#' continue end
		for (dir, offset) in enumerate([CartesianIndex(0,-1), CartesianIndex(-1, 0)])
			ix2 = ix + offset
			if checkbounds(Bool, g, ix2)
				d = Bool(dir - 1)
				push!(entries, (code_for(m, ix2=>d), code_for(m, ix=>d), 1))
			end
		end
		push!(entries, (code_for(m, ix=>false), code_for(m, ix=>true), 1000))
	end
	endof = findfirst(isequal('E'), g)
	start = findfirst(isequal('S'), g)
	maze_end = m.counter + 1
	append!(entries, [(code_for(m, endof=>d), maze_end, 1) for d in [true, false]])
	graph = sparse(unzip(entries)..., maze_end, maze_end)
	graph = graph + graph'
	min_cost, preds = dijkstra(graph, code_for(m, start=>false), maze_end)
	pred_codes = get_preds(preds, maze_end)
	from_code = Dict(v=>k[1] for (k,v) in m.dict)
	length(Set([from_code[c] for c in pred_codes if haskey(from_code, c)]))
end

As part two of the problem requires all shortest paths rather than just a single one, we have to use a modified version of Dijkstra’s algorithm.

function dijkstra(g, start, endof)
	costs = Dict{Int, Float64}()
	preds = DefaultDict{Int, Vector{Int}}(Vector{Int})
	q = BinaryHeap(Base.By(last), [(0, start, 0.0)])
	while !isempty(q)
		pred,u,c = pop!(q)
		if haskey(costs, u) && u == endof && c > costs[u]
			return costs[endof], preds
		elseif !haskey(costs, u) || costs[u] == c
			push!(preds[u], pred)
			if !haskey(costs, u)
				costs[u] = c
				for (v,w) in zip(findnz(g[:, u])...)
					push!(q, (u, v, w + c))
				end
			end
		end
	end
	costs[endof], preds
end

Day 17 Here, we’re simulating a fictional CPU instructions set.

combo(operand, registers) = operand >= 4 ? registers[operand - 3] : operand

function step(op, operand, registers, results, pc)
	if op == 0
		registers[1] = div(registers[1], 1 << combo(operand, registers))
	elseif op == 1
		registers[2] = xor(registers[2], operand)
	elseif op == 2
		registers[2] = combo(operand, registers) & 0b111
	elseif op == 3
		if registers[1] != 0
			return operand
		end
	elseif op == 4
		registers[2] = xor(registers[2], registers[3])
	elseif op == 5
		push!(results, combo(operand, registers) & 0b111)
	elseif op == 6
		registers[2] = div(registers[1], 1 << combo(operand, registers))
	elseif op == 7
		registers[3] = div(registers[1], 1 << combo(operand, registers))
	end
	pc + 2
end

function advent17(prog, registers)
	results = []
	pc = UInt(0)
	while true
		pc = doit(prog[pc + 1], prog[pc + 2], registers, results, pc)
		if pc >= length(prog)
			return results
		end
	end
end

The second part of the problem is to identify the register value that would let a specific program for this instruction set be a Quine.

function advent17_2()
	A_base = UInt64(0)
	for c in Iterators.reverse(UInt8[2,4, 1,2, 7,5, 1,7 ,4,4, 0,3 ,5,5 ,3,0])
		A_base = A_base << 3
		options = map(0b0:0b111) do abit
			A = A_base + abit
			7 & (7  (2  (A & 7))  (A >> (2  (A & 7))))
		end
		println(Int.(sort(options)))
		abit = findfirst(0b0:0b111) do abit
			A = A_base + abit
			c == 7 & (7  (2  (A & 7))  (A >> (2  (A & 7))))
		end
		A_base |= (abit - 1)
	end
	A_base
end

Day 18

Find the shortest path in a grid given a list of obstructions.

function advent18(locs, dims)
	grid = ones(Bool, dims)
	grid[locs] .= false
	dijkstra(grid, CartesianIndex(1,1), CartesianIndex(dims))
end

Day 19

Count the number of ways you can construct text out of substrings in patterns.

function advent19(patterns, text)
	nways = Array{Int}(undef, length(text) + 1)
	nways[1] = 1
	for i in 1:length(text)
		nways[i + 1] = sum(nways[i - length(p) + 1] for p in patterns
			if length(p) <= i && text[i - length(p) + 1 : i] == p; init=0)
	end
	nways[end]
end

Day 23

Find the number of 3-cliques where at least one of the node ids starts with ‘t’. This is the same as subtracting the number of 3-cliques where no node id starts with ‘t’ from the total number of 3-cliques. To find the number of 3-cliques, we can just raise the adjacency matrix to the third power. Each loop will counted twice per node: once clockwise, once counter clockwise. Each loop is also counted by three different nodes.

cliques(g) = sum([amt / 2 for (ix, amt) in zip(findnz(diag(g^3))...)]) / 3

function advent23(f)
    m = IntMapper{SubString{String}}()
    edges = [code_for.(Ref(m), split(line, '-')) for line in readlines(f)]
    g = sparse(first.(edges), last.(edges), ones(length(edges)))
    g2 = g + g'
    mask = .~(startswith.(codes(m), 't'))
    cliques(g2) - cliques(g2[mask, mask])
end

Day 24

Simulate a DAG of logical operators.

struct Op{F}
    func::F
    inputs::Vector{String}
    output::String
    Op(f::F, a, b) where {F} = new{F}(f, String.(a), String(b))
end

Base.@kwdef struct Prog
    vals::DefaultDict{String,Int,Int} = DefaultDict{String, Int}(0)
    ops::Vector{Op} = []
end

function advent24(file)
    p = Prog()
    for line in eachline(file)
        process!(p, line)
    end
    while true
        old_vals = copy(p.vals)
        sim!(p)
        if old_vals == p.vals
            break
        end
    end
    bits = sort(collect(filter(keys(p.vals)) do a
        startswith(a, "z")
    end))
    sum([p.vals[b] << (i-1) for (i, b) in enumerate(bits)])
end

function sim!(p)
    for op in p.ops
        p.vals[op.output] = op.func([p.vals[a] for a in op.inputs]...)
    end
end

function process!(p, line)
    if !isnothing(local a = match(r"([^:]+): (\d)", line))
        p.vals[a.captures[1]] = parse(Int, a.captures[2])
    elseif !isnothing(local a = match(r"(\w+) XOR (\w+) -> (\w+)", line))
        push!(p.ops, Op(xor, a.captures[1:2], a.captures[3]))
    elseif !isnothing(local a = match(r"(\w+) AND (\w+) -> (\w+)", line))
        push!(p.ops, Op(&, a.captures[1:2], a.captures[3]))
    elseif !isnothing(local a = match(r"(\w+) OR (\w+) -> (\w+)", line))
        push!(p.ops, Op(|, a.captures[1:2], a.captures[3]))
    end
end

Day 25

function advent25(grids)
	counter = 0
	locks = []
	keys = []
	for grid in grids
		if all(grid[1, :] .== '#')
			push!(locks, findfirst.(isequal('.'), eachcol(grid)) .- 2)
		else
			push!(keys, size(grid, 1) .- findfirst.(isequal('#'), eachcol(grid)))
		end
	end
	for l in locks
		for k in keys
			if all(l .+ k .<= 5)
				counter += 1
			end
		end
	end
	counter
end

Updated: