Advent of Code 2024
Last updated: Dec 19, 2024.
I’ve always attempted problems from the advent of code with the goal of writing the clearest code I can. This year I decided to try it in Clojure. I read SICP as an impressionable young programmer and thus some part of my brain always thinks in Lisp. Clojure is great for advent of code for a couple of reasons:
- The immutable sequence abstraction makes it easy to express many computations with great clarity.
- The iterative REPL-based development experience is a joy. If you’ve never used a Lisp it’s hard to describe how uniquely satisfying it is to test tiny pieces of code incrementally and assemble them into the solution in a Lisp-family language.
For each of the problems below I’ll restate the problem in a concise way and describe the non-obvious parts of the solution in words. Wherever necessary, I’ll also indicate the result of evaluating a Lisp expression in this way:
(+ 4 5) 9
Day 1
Part 1: Given two columns of numbers, sort them both and compute pairwise absolute differences and return the sum of all differences.
Most of the work in solving this problem is just reading the input:
defn slurp-two-columns
("Returns the two columns of integers from the input file"
[input-path]let [lines (string/split (slurp input-path) #"\n")
(map #(map Integer/parseInt
pairs (% #"\s+"))
(string/split
lines)
pairs-interleaved (flatten pairs)take-nth 2 pairs-interleaved)
col1 (take-nth 2 (rest pairs-interleaved))]
col2 ( [col1 col2]))
A couple of Clojure features make the solution very easy to read: (1) map
in Clojure takes any number of sequences as arguments (2) anonymous functions can be written as #(...)
with %1
and %2
as placeholders for the arguments.
defn solve-day1-part1
(
[input-path]let [[col1 col2] (slurp-two-columns input-path)]
(reduce + 0
(map #(abs (- %1 %2))
(sort col1) (sort col2))))) (
(solve-day1-part1 “src/code/data/advent2024-1.txt”) 3508942
Part 2: Calculate a total similarity score by adding up each number in the left list after multiplying it by the number of times that number appears in the right list.
frequencies
returns a map (dictionary) of items in a sequence mapped to the number of times they appear.
defn solve-day1-part2
(
[input-path]let [[col1 col2] (slurp-two-columns input-path)
(frequencies col2)
counts (map #(* % (get counts % 0)) col1)]
scores (reduce + 0 scores))) (
(solve-day1-part2 “src/code/data/advent2024-1.txt”) 26593248
Day 2
Part 1: Each line is a list of numbers called a “report”. A report is safe if both are true:
- The numbers are either all increasing or all decreasing.
- Any two adjacent levels differ by at least one and at most three.
First we write a function to read the rows of integers:
defn slurp-rows
("Returns the rows of integers from the input file"
[input-path]let [lines (string/split (slurp input-path) #"\n")]
(map #(map Integer/parseInt (string/split % #"\s+")) lines))) (
For each row we will compute the successive differences and call it deltas
. This will help us check if the row meets both of the conditions.
defn deltas [xs] (map - (rest xs) (drop-last xs))) (
(deltas [1 2 3 4 1]) => (1 1 1 -3)
The first condition can be understood as all the deltas having the same sign. We’ll define sign
that returns -1, 0, or 1 indicating the sign of a number. The function same-sign?
implements the first condtion.
defn sign [x] (if (zero? x) 0 (/ x (abs x))))
(defn same-sign? [r] (apply = (map sign (deltas r)))) (
(same-sign? [1 2 3 4 5]) => true (same-sign? [5 4 3 2 1]) => true (same-sign? [5 4 3 2 7]) => false
The second condition:
defn bounded-deltas? [r]
(every? #(and (>= (abs %) 1) (<= (abs %) 3))
( (deltas r)))
Now we go through every report and check it against both conditions, and count number of reports that are safe.
defn safe-report? [r]
(and (same-sign? r) (bounded-deltas? r)))
(
defn solve-day2-part1
(
[input-path]count (filter safe-report? (slurp-rows input-path)))) (
(solve-day2-part1 “src/code/data/advent2024-2.txt”) 269
Part 2: Count the number of safe reports, but now a report is considered safe if removing a single level from it renders it safe according to the two rules as per part 1.
We’ll write a function that returns all reports that result from dropping a single level.
defn filter-one
(
[r]for [i (range (count r))]
(concat (take i r) (drop (inc i) r)))) (
Clojure has a cool feature called a threading macro. This allows us to write the solution as a pipeline that reads very naturally: slurp-rows -> filter -> count
.
defn solve-day2-part2
(
[input-path]->> (slurp-rows input-path)
(filter #(some safe-report? (filter-one %)))
(count))
(solve-day2-part2 “src/code/data/advent2024-2.txt”) 337
Day 3
Part 1: Scan the input and identify every instance of
mul(a, b)
. Computea * b
and sum all such results.
re-seq
returns a sequence of all the matches for the given regex. The groups of the match can be easily extracted using destructuring in the let
binding: [[_ a b] match]
.
defn solve-day3-part1
(
[input-path]reduce + 0
(map #(let [[_ a b] %]
(* (Integer/parseInt a) (Integer/parseInt b)))
(re-seq #"mul\((\d+),(\d+)\)" (slurp input-path))))) (
(solve-day3-part1 “src/code/data/advent2024-3.txt”) 171183089
Part 2: The input now includes two new kinds of instructions.
don't()
disables futuremul
instructions whiledo()
enables them. Only the most recentdo()
ordon't()
instruction applies. At the beginning of the program, mul instructions are enabled.
Each instruction is one of:
[:mul a b]
:dont
:do
State is [enabled? total]
Reducer is:
defn process-instruction
(
[result inst]let [[enabled sum] result]
(
(match inst:do [true sum]
:dont [false sum]
:mul a b] (if enabled
[+ sum (* a b))]
[enabled (
[enabled sum]):else result)))
Parse the input
defn parse-instructions
(
[input]let [matches (re-seq #"mul\((\d+),(\d+)\)|do\(\)|don't\(\)"
(
input)]map #(let [[text a b] %]
(cond
("mul") [:mul
(.startsWith text
(Integer/parseInt a)
(Integer/parseInt b)]= text "don't()") :dont
(= text "do()") :do))
( matches)))
(parse-instructions “xmul(2,4)&mul[3,7]!^don’t()_mul(5,5)+mul(32,64](mul(11,8)undo()?mul(8,5))”) ([:mul 2 4] :dont [:mul 5 5] [:mul 11 8] :do [:mul 8 5])
defn solve-day3-part2
(
[input-path]reduce process-instruction [true 0]
(slurp input-path)))) (parse-instructions (
(solve-day3-part2 “src/code/data/advent2024-3.txt”) [false 63866497]