Advent of Code 2024
By Brendan Halpin
Here are my solutions to the 2024 Advent of Code, using Julia.
Day 1
using CSV, DataFrames
df = CSV.read("advent1.csv", DataFrame, header=["L1", "L2"], delim=" ")
println("Day1, Answer 1: ", sum(abs.(sort(df.L1) - sort(df.L2))))
println("Day1, Answer 2: ", sum([i*count(x->(x == i), df.L2) for i in df.L1]))
Day 2
Part 1
Read the data
records = [[parse(Int32, x) for x in field]
for field in [split(r, " ")
for r in readlines("advent2.csv")]]
Calculate the changes
delta = [x[2:end] - x[1:end-1] for x in records]
Count the conditions
range1_3 = [all(x -> (abs(x)>=1) & (abs(x)<=3), d) for d in delta]
declining = [all(x -> x < 0, d) for d in delta]
rising = [all(x -> x > 0, d) for d in delta]
safe = range1_3 .& (declining .| rising)
println("$(sum(safe)) records are safe")
Part 2
Create functions for tidiness (could have used above)
function rangegood(l)
all(x -> (abs(x)>=1) & (abs(x)<=3), l)
end
function strictmono(l)
all(x -> x < 0, l) | all(x -> x > 0, l)
end
For each unsafe record, drop each element one at a time, calculate the changes and test. If dropping any element makes the record pass, it passes as a whole. Testing by dropping all elements is redundant (after the first pass) but breaking out saves negligible time.
safe2 = copy(safe)
for i in 1:1000
if !safe[i]
for j in 1:length(records[i])
dropone = records[i][setdiff(1:length(records[i]), j)]
deltad1 = dropone[2:end] - dropone[1:end-1]
if rangegood(deltad1) & strictmono(deltad1)
safe2[i] = true
end
end
end
end
println("$(sum(safe2)) records are safe if we allow one exception")
Day 3
# Part 1
## Read the data, joining the lines
data = join(readlines("advent3.dat"))
## Create a function
mul(a,b) = a*b
# Regular expressions, and treating data as code:
result1 = sum([eval(Meta.parse(m.match)) for m in eachmatch(r"mul\([0-9][0-9]?[0-9]?,[0-9][0-9]?[0-9]?\)", data)])
println("First calculation comes to $result1")
# Part 2
## first pass: regexp to remove don't() to do()
## second: if there is one last don't() without a do() after, remove that to end
datanew = replace(data, r"don't\(\).*?do\(\)" => "")
datanew = replace(datanew, r"don't\(\).*?$" => "")
result2 = sum([eval(Meta.parse(m.match)) for m in eachmatch(r"mul\([0-9][0-9]?[0-9]?,[0-9][0-9]?[0-9]?\)", datanew)])
println("Second calculation comes to $result2")
Day 4
Part 1
Awkward testxmas()
function needs to test that we are not too near an edge, before extracting a text-slug and comparing it to “XMAS”.
data = readlines("advent4.dat")
nlines = length(data)
ncols = length(data[1])
xloc = [(i, j) for i in 1:nlines for j in 1:ncols if data[i][j] == 'X']
function testxmas(row, col, rowdir, coldir)
if (row+3*rowdir > 0) &
(row+3*rowdir <= nlines) &
(col+3*coldir > 0) &
(col+3*coldir <= ncols)
slug = join([data[row+i*rowdir][col+i*coldir] for i in 0:3])
if slug=="XMAS"
return(true)
else
return(false)
end
else
return(false)
end
end
count = sum([testxmas(loc..., idir, jdir)
for loc in xloc
for idir in -1:1
for jdir in -1:1])
println("We find $count XMASes")
Part 2
Harder to understand the instructions, but less code for part 2. Avoid edges by iterating from 2 to end-1.
function itsX(i,j)
Set([data[i-1][j-1], data[i+1][j+1]]) == Set([data[i-1][j+1], data[i+1][j-1]]) == Set(['M','S'])
end
println("X-MASes: $(sum([itsX(i,j)
for i in 2:nlines-1
for j in 2:ncols-1 if data[i][j] == 'A']))")
Day 5
Part1
Strategy: for each sequence, calculate all pairs and see if their reverse is in the rule set
## Read rules and sequences
rules = [parse.(Int32,split(x, "|")) for x in readlines("advent5rules.dat")]
updates = [[parse(Int32, x) for x in split(r, ",")] for r in readlines("advent5.dat")]
# Function: test a sequence against the rules & sum the infringements
function disordered(seq)
sum([[seq[j], seq[i]] in rules for i in 1:length(seq)-1 for j in i+1:length(seq)])
end
nInfringe = [disordered(u) for u in updates]
answer1 = sum([seq[[Int(floor(length(seq)/2)) + 1]] for seq in updates[nInfringe.==0]])
Part 2
For all ordered (not necessarily consecutive) pairs, swap them if they infringe, repeating until the result is in order
incorrects = updates[nInfringe.>0]
nInfringe[nInfringe.>0]
corrected = []
for incorrect in incorrects
temp = copy(incorrect)
while disordered(temp)>0
for i in 1:length(incorrect)-1
for j in i+1:length(incorrect)
# Swap pair if infringing
if [temp[j], temp[i]] in rules
temp[[i,j]] = temp[[j,i]]
end
end
end
end
append!(corrected, [temp])
end
answer2 = sum([seq[Int(floor(length(seq)/2)) + 1] for seq in corrected])
println("Part 1: $answer1")
println("Part 2: $answer2")
Day 6
Part 1
rawlayout = readlines("advent6.dat")
layout = zeros(Int32,length(rawlayout), length(rawlayout[1]))
global gloc = [0,0]
for i in 1:length(rawlayout)
for j in 1:length(rawlayout[1])
if rawlayout[i][j] == '#'; layout[i,j] = 1; end
if occursin(rawlayout[i][j], "^>v<"); global gloc = [i,j]; end
end
end
global dir = 0
for i in 1:4
if rawlayout[gloc[1]][gloc[2]] == "^>v<"[i]; global dir = i
end
end
function moveguard(guard; mlayout = layout)
# 1 up, 2 left, 3 down, 4 right
i, j, dir = guard
if dir == 1; i -= 1
elseif dir == 2; j += 1
elseif dir == 3; i += 1
elseif dir == 4; j -=1
end
if (i in range(1,size(mlayout)[1])) &
(j in range(1,size(mlayout)[2]))
if (mlayout[i,j] != 1)
return([i,j,dir])
else
newguard = guard
newguard[3] = 1 + mod(dir, 4)
moveguard(guard)
end
else
return(-1)
end
end
Guard = [gloc..., dir]
track = []
while Guard != -1
global Guard
append!(track, [Guard[1:2]])
Guard = moveguard(Guard)
end
println("Answer 1: $(length(unique(track)))")
Part 2
Part 2 is driving me mad. I get the correct answer on the training data but not on the real data!
function maketrack(initpos; layout=layout)
result = ""
coords = copy(initpos)
track = []
while coords != -1
coords = moveguard(coords, layout = layout)
if coords == -1
result = "EXIT"
break
elseif coords in track
result = "LOOP"
break
end
append!(track, [coords])
end
return(result)
end
function testloc(loc; layout = layout, gloc=gloc, dir=dir)
if layout[loc...] == 0
templo = copy(layout)
templo[loc...] = 1
maketrack([gloc..., dir], layout=templo)
else
"Null"
end
end
origtrack = unique(track)
exits = []
nulls = []
loops = []
Threads.@threads for block in origtrack
outcome = testloc(block)
if outcome == "LOOP"
append!(loops, [block])
elseif outcome == "EXIT"
append!(exits, [block])
elseif outcome == "Null"
append!(nulls, [block])
else
println("Anomaly: $(block)")
end
end
println("Exits: $(length(exits)); Loops: $(length(loops))")
Part 2 revisited
Day 6 part 2 really got me stuck. Moreso in that I was using a very inefficient algorithm that worked through the walkgrid cell-by-cell and took forever to complete (even with parallel processing). That’s something that makes debugging particularly difficult!
Then I realised I could speed it up by looking ahead to the next obstacle, rather than proceeding step by step. This movefast()
function is key:
function movefast(guard; mlayout = layout)
# 1 up, 2 left, 3 down, 4 right
i, j, dir = guard
## route to next obstacle or edge
exit = false
idistance = 0
jdistance = 0
if dir == 1 # up, i reduces
route = reverse(mlayout[1:i, j])
if maximum(route) == 0
exit = true
idistance = -(length(route) - 1)
else
idistance = -(argmax(route) - 2)
end
elseif dir == 2
route = mlayout[i, j:end]
if maximum(route) == 0
exit = true
jdistance = length(route) - 1
else
jdistance = argmax(route) - 2
end
elseif dir == 3
route = mlayout[i:end, j]
if maximum(route) == 0
exit = true
idistance = length(route) - 1
else
idistance = argmax(route) - 2
end
elseif dir == 4
route = reverse(mlayout[i, 1:j])
if maximum(route) == 0
exit = true
jdistance = -(length(route) - 1)
else
jdistance = -(argmax(route) - 2)
end
end
return((exit, [i+idistance, j+jdistance, 1 + mod(dir, 4)]))
end
This required changing some of the logic. The track that results is a set of legs, not a list of successive cells. This code yields the same answer as the Part 1 code above:
Guard = [gloc..., dir]
texit = false
mftrack = [Guard]
while texit != true
texit, Guard = movefast(Guard)
append!(mftrack, [Guard])
end
walkgrid = zeros(Int32, size(layout)...)
oldpoint = track[1][1:2]
walkgrid[oldpoint...] = 1
for elt in 2:length(track)
for i in min(track[elt-1][1], track[elt][1]):max(track[elt-1][1], track[elt][1])
for j in min(track[elt-1][2], track[elt][2]):max(track[elt-1][2], track[elt][2])
walkgrid[i,j] = 1
end
end
end
println("Answer 1: $(sum(walkgrid))")
The testloc()
function introduces an obstacle and traces the progress of the guard. If one of the legs of the track duplicates an existing one, a loop is detected.
function testloc(block; gloc = gloc, dir = dir)
tlguard = [gloc..., dir]
texit = false
isloop = false
track = [tlguard]
templo = copy(layout)
templo[block...] = 1
while texit != true
texit, tlguard = movefast(tlguard, mlayout=templo)
if tlguard in track
append!(track, [tlguard])
isloop=true
break
else
append!(track, [tlguard])
end
end
(isloop, track)
end
The walkgrid is important: where its value is 1 marks places it makes sense to pose an obstacle. So we iterate through those locations and count the loops.
ncols, nrows = size(walkgrid)
loops = []
exits = []
for i in 1:ncols, j in 1:nrows
if walkgrid[i,j] == 1
if testloc([i,j])[1]
push!(loops, [i,j])
else
push!(exits, [i,j])
end
end
end
println("Answer 2: $(length(loops))")
Bonus: plotting the scene
Plot the guards’ standard walk:
function plotwalk(track, layout)
y = [elt[1] for elt in track]
x = [elt[2] for elt in track]
plot(heatmap(layout), aspect_ratio=:equal, yaxis=:flip, colorbar=false, size=(800,800))
plot!(x, y)
scatter!([track[1][2]], [track[1][1]], color=:green, label="Start")
scatter!([track[end][2]], [track[end][1]], color=:red, label="End")
end
plotwalk(mftrack, layout)
plot!(title="Guards' walk")
savefig("day6a.png")
Plot a walk with a loop due to an obstacle
templo = copy(layout)
templo[68, 40] = 1 # <--- obstacle
texit = false
tlguard = [gloc..., dir]
tltrack = [tlguard]
while texit != true
texit, tlguard = movefast(tlguard, mlayout=templo)
if tlguard in tltrack
append!(tltrack, [tlguard])
isloop=true
break
else
append!(tltrack, [tlguard])
end
end
plotwalk(tltrack, templo)
plot!(title="Obstacle at 68, 40 causes loop")
savefig("day6b.png")
Day 7
I spent ages thinking ineffectively about this, and then found a quick idiomatic solution.
data = [split(line) for line in readlines("advent7.dat")]
results = [parse(Int64, x[1][1:end-1]) for x in data]
coefs = [[parse(Int64, x) for x in line[2:end]] for line in data]
rlist = []
for seq in coefs
result = seq[1]
for i in 2:length(seq)
result = vcat([r + seq[i] for r in result],[r*seq[i] for r in result])
end
append!(rlist, [result])
end
println("Answer day 7, part 2: ", sum([results[i]*(results[i] in rlist[i]) for i in 1:length(coefs)]))
Moreover, the extension to part 2 was easy.
## Part 2
function concatop(a, b)
parse(Int64, "$a$b")
end
r2list = []
for seq in coefs
result = seq[1]
for i in 2:length(seq)
result = vcat([r + seq[i] for r in result],
[r * seq[i] for r in result],
[concatop(r,seq[i]) for r in result])
end
append!(r2list, [result])
end
println("Answer day 7, part 1: ", sum([results[i]*(results[i] in r2list[i]) for i in 1:length(coefs)]))
Day 8
Part 1
Read the data (symbols get mapped to integers)
rawlayout = readlines("advent8.dat")
nrows = length(rawlayout); ncols = length(rawlayout[1])
layout = zeros(Int32, nrows, ncols)
for i in 1:nrows
for j in 1:ncols
if rawlayout[i][j] != '.'
layout[i,j] = rawlayout[i][j]
end
end
end
Antinodes are defined like this
function antinodes(a, b)
deltai = b[1] - a[1]
deltaj = b[2] - a[2]
[[a[1]-deltai, a[2] - deltaj],
[b[1]+deltai, b[2] + deltaj]]
end
For each frequency, build a list of locations in a Dict
antennae = Dict()
for i in 1:nrows, j in 1:ncols
ant = layout[i,j]
if ant != 0
if haskey(antennae,ant)
push!(antennae[ant], [i,j])
else
antennae[ant] = [[i,j]]
end
end
end
For each frequency, for each pair of antennae, calculate antinodes and append to a list
locs1 = []
for key in keys(antennae) # For every freqeuency
aset = antennae[key]
for i in 1:length(aset)-1, j in i+1:length(aset) # for every pair of antennae
append!(locs1, antinodes(aset[i], aset[j]))
end
end
Filter out the off-grid antinodes, and count unique observations:
locs1a = [l for l in locs1 if (l[1] in 1:nrows) & (l[2] in 1:ncols)]
println("Answer 1: $(length(unique(locs1a)))")
Part 2
A new function to define new antinodes: All elements of the line with slope deltai/deltaj that passes through the pair, that are integer values.
function antinodesp2(a, b)
deltai = b[1] - a[1]
deltaj = b[2] - a[2]
slope = deltai/deltaj
lineelts = [[a[1] + (x-a[2])*slope, Int(x)] for x in 1:ncols]
[floor.(Int,elt) for elt in lineelts if floor(elt[1])==elt[1]]
end
Analagous steps to summarise
locs2 = []
for key in keys(antennae)
aset = antennae[key]
for i in 1:length(aset)-1, j in i+1:length(aset)
append!(locs2, antinodesp2(aset[i], aset[j]))
end
end
locs2a = [l for l in locs2 if (l[1] in 1:nrows) & (l[2] in 1:ncols)]
println("Answer 2: $(length(unique(locs2a)))")