Advent of Code 2024 – Day 2
Part 1
Given a collection of sequences of numbers, determine how many sequences meet the criteria (described as “are safe”):
- Either
- The sequence is increasing
- The sequence is decreasing
- The sequence change is at least 1 and at most 3
After grabbing the input data, it's worth noting that the sequences are not consistently sized. They vary in length between 5 and 8 numbers, and unlike Day 1, the numbers have varying numbers of digits.
I feel like parse_input
could have been implemented using map
on the iterables returned by eachline
and eachsplit
, but the for
/push!
combination was Good Enough™.
I couldn't find a built in Julia function to iterate over a collection performing a comparison to the previous element, so I wrote one. The first attempt was implementing all of is_increasing
, then I realized it could be used with a functor, which is The Julia Way™.
The is_change_in_range
test uses the chaining comparisons feature of Julia, which is really pleasant to write. It also uses the do
syntax for declaring a functor, which is part of why applying a functor is The Julia Way™.
function parse_input(src = "input.txt")
sequences = Vector{Vector{Int}}(undef, 0)
for line in eachline(src)
sequence = Vector{Int}(undef, 0)
for digits in eachsplit(line)
push!(sequence, parse(Int, digits))
end
push!(sequences, sequence)
end
sequences
end
# Compare each element of a sequence with the previous element
# If the comparison function returns false, return false immediately
# Otherwise return true
function compare_sequence_elements(f, seq)
@assert length(seq) > 1
prev = seq[begin]
for cur in seq[begin+1:end]
if(!f(prev, cur))
return false
end
prev = cur
end
return true
end
is_increasing(seq) = compare_sequence_elements(<, seq)
is_decreasing(seq) = compare_sequence_elements(>, seq)
function is_change_in_range(seq, min_change = 1, max_change = 3)
compare_sequence_elements(seq) do prev, cur
diff = abs(prev - cur)
return min_change <= diff <= max_change
end
end
is_safe(seq) = (is_increasing(seq) || is_decreasing(seq)) && is_change_in_range(seq)
function main_part1()
coll = parse_input()
println("Safe reports:", count(is_safe, coll))
end
main_part1()
Part 2
This continues Part 1 by adding another condition under which a sequence can be considered “safe”, named the “Problem Dampener”.
If removing a single element from the sequence makes it pass the conditions, then it should be considered “safe”.
is_safe_with_dampener
makes use of Julia's great index range handling. begin
and end
make accessing the heads & tails of collections concise, and the code takes advantage of Julia's behavior of returning an empty collection if you provide a second index that is less than the first (without specifying a negative step).
I also like that because the code uses eachindex
, it doesn't care if the sequence is 1 indexed (the Julia default), or some other starting point. By using prevind
and nextind
, the code doesn't assume the sequence is using linear indexing. During compilation, these all get substituted with what is appropriate for the sequence, so there's no performance overhead for writing more generic code.
function is_safe_with_dampener(seq)
if is_safe(seq)
return true
end
# Test the sequence, removing an element
# If any subsequence returns true, return true
for ind in eachindex(seq)
subseq = vcat(seq[begin:prevind(seq,ind)], seq[nextind(seq,ind):end])
if is_safe(subseq)
return true
end
end
return false
end
function main_part2()
coll = parse_input()
println("Safe reports (with dampener):", count(is_safe_with_dampener, coll))
end
main_part2()