Advent of Code 2023 Day 8

Let's solve today's challenge using Clojure.

Parsing


            LLR

            AAA = (BBB, BBB)
            BBB = (AAA, ZZZ)
            ZZZ = (ZZZ, ZZZ)
            

sample input

We will try to be expressive and going to parse the input into a semantic form. We are mapping node directions to :left and :right keys and parsing out the directions using same keywords, so later we would be able to query our structure with ease.


              (defn parse [input]
                (let [[directions maps] (str/split input #"\n\n")
                      maps (reduce #(conj %1 (read-direction %2)) {} (str/split-lines maps))
                      directions (map (fn [itm] (case itm
                                                  "L" :left
                                                  "R" :right)) (str/split directions #""))]
                  {:directions directions, :maps maps}))
            

parse


              (defn read-direction [line]
                (let [[key left right] (re-seq #"[A-Z0-9][A-Z0-9][A-Z0-9]" line)]
                  {key {:left left :right right}}))
            

read-direction

And voila, our resulting map:


             {:directions (:left :left :right),
              :maps
                {"AAA" {:left "BBB", :right "BBB"},
                 "BBB" {:left "AAA", :right "ZZZ"},
                 "ZZZ" {:left "ZZZ", :right "ZZZ"}}}
            

semantic input

Part one

Part one is pretty straightforward, we just loop increasing the counter until "ZZZ" is found.


            (defn count-steps [{directions :directions maps :maps}]
              (loop [cnt 0
                     [direction & more] (flatten (repeat directions))
                     location "AAA"] ; Starting at AAA
                (if (= location "ZZZ")
                  cnt
                  (recur (inc cnt) more (get-in maps [location direction])))))
            

count-steps

Neat part is how we can leverage infinite sequence to repeat our directions list:


            (flatten (repeat directions))
            

infinite directions

Assemble parsing and solving together:


            (defn part-one [input]
              (-> input parse count-steps))

            (part-one test-input) ;; => 6
            

part one

Part two

Now part two is a bit more tricky. Unless you have an access to a quantum computer, brute-forcing the solution might finish just after the heat death of the universe.

As an experiment we can check how far apart are subsequent exit nodes on a single path. Here we use real input and start from "DVA" node. We halt after 5 nodes are found.


  (defn find-repetitions [{directions :directions maps :maps} start]
    (loop [reps-cnt 0
           reps-acc []
           cnt 0
           [direction & more] (flatten (repeat directions))
           location start]
      (cond
        (< 5 reps-cnt) reps-acc
        (str/ends-with? location "Z")
        (recur
         (inc reps-cnt)
         (conj reps-acc cnt)
         0
         more
         (get-in maps [location direction]))
        :else
        (recur
         reps-cnt
         reps-acc
         (inc cnt)
         more
         (get-in maps [location direction])))))

  (find-repetitions (parse real-input) "DVA")
      ;; => [13939 13938 13938 13938 13938 13938]
            

experiment

The distances are identical — the paths are endless circles!

Repeating integer sequences' converging point is their lowest common multiple (LCM). With newly acquired information, we calculate all paths' lengths and LCM of all of them.


             (defn find-repetition [{directions :directions maps :maps} start]
              (loop [cnt 0
                     [direction & tail] (flatten (repeat directions))
                     location start]
                (if (str/ends-with? location "Z")
                  cnt
                  (recur (inc cnt) tail (get-in maps [location direction])))))

            (defn gcd [a b]
              (if (zero? b) a (recur b, (mod a b))))

            (defn lcm [a b]
              (/ (* a b) (gcd a b)))

            (defn part-two [input]
              (let [parsed (parse input)]
                (->> (parsed :maps)
                     keys
                     (filter #(str/ends-with? % "A"))
                     (map #(find-repetition parsed %))
                     (reduce lcm)))
            

part two

And we get our answer. Good luck next day!


              (part-2 real-input) ;; => 13740108158591
            

answer