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:

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")
        pairs (map #(map Integer/parseInt
                         (string/split % #"\s+"))
                   lines)
        pairs-interleaved (flatten pairs)
        col1 (take-nth 2 pairs-interleaved)
        col2 (take-nth 2 (rest pairs-interleaved))]
    [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)
        counts (frequencies col2)
        scores (map #(* % (get counts % 0)) col1)]
    (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). Compute a * 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 future mul instructions while do() enables them. Only the most recent do() or don'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
                   [enabled (+ sum (* a b))]
                   [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
             (.startsWith text "mul") [:mul
                                       (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]
          (parse-instructions (slurp input-path))))

(solve-day3-part2 “src/code/data/advent2024-3.txt”) [false 63866497]