🎄 Advent of Clerk: Day 5

(ns advent-of-clerk.day-05
(:require #?@(:bb [] :clj [[nextjournal.clerk :as clerk]])
[advent-of-clerk.utils :as utils]
[clojure.string :as cstr]))
(def input (->> (utils/load-input "day_05.txt")
(cstr/split-lines)))
["
[B] [B] [S] "
"
[M] [P] [L] [B] [J]"
"
[D] [R] [V] [D] [Q] [D]"
"
[T] [R] [Z] [H] [H] [G] [C]"
"
[P] [W] [J] [B] [J] [F] [J] [S]"
"
[N] [S] [Z] [V] [M] [N] [Z] [F] [M]"
"
[W] [Z] [H] [D] [H] [G] [Q] [S] [W]"
"
[B] [L] [Q] [W] [S] [L] [J] [W] [Z]"
"
1 2 3 4 5 6 7 8 9 "
"
"
"
move 3 from 5 to 2"
"
move 5 from 3 to 1"
"
move 4 from 4 to 9"
"
move 6 from 1 to 4"
"
move 6 from 8 to 7"
"
move 5 from 2 to 7"
"
move 1 from 5 to 4"
"
move 11 from 9 to 7"
"
move 1 from 1 to 9"
"
move 6 from 4 to 6"
494 more elided]
(def ex-input
[" [D] "
"[N] [C] "
"[Z] [M] [P]"
" 1 2 3 "
""
"move 1 from 2 to 1"
"move 3 from 1 to 3"
"move 2 from 2 to 1"
"move 1 from 1 to 2"])
["
[D] "
"
[N] [C] "
"
[Z] [M] [P]"
"
1 2 3 "
"
"
"
move 1 from 2 to 1"
"
move 3 from 1 to 3"
"
move 2 from 2 to 1"
"
move 1 from 1 to 2"
]

Part 1

(defn parse-stacks [input]
(let [uppercase (set (map char
(range (int \A) (inc (int \Z)))))]
(->> input
(map (comp (partial into {})
(partial map-indexed
(fn [i xs]
(let [ids (filter uppercase xs)]
(when-not (empty? ids)
[(inc i) ids]))))
(partial partition-all 4)))
(apply merge-with concat))))
#object[advent_of_clerk.day_05$parse_stacks 0x4147f46d "
advent_of_clerk.day_05$parse_stacks@4147f46d"
]
(defn parse-instructions [input]
(map (comp vec
(partial map parse-long)
(partial re-seq #"\d+")) input))
#object[advent_of_clerk.day_05$parse_instructions 0x6aaec13 "
advent_of_clerk.day_05$parse_instructions@6aaec13"
]
(defn parse [input]
(let [[setup instrs] (take-nth 2 (partition-by empty? input))]
{:stacks (parse-stacks (butlast setup))
:instrs (parse-instructions instrs)}))
#object[advent_of_clerk.day_05$parse 0x4e04779b "
advent_of_clerk.day_05$parse@4e04779b"
]
(defn cratemover9000 [stacks [n from to]]
(reduce (fn [stacks _]
(-> stacks
(update to #(cons (first (stacks from)) %))
(update from rest)))
stacks
(range n)))
#object[advent_of_clerk.day_05$cratemover9000 0x348e200e "
advent_of_clerk.day_05$cratemover9000@348e200e"
]
(defn run-instructions [crate-mover data]
(reduce (fn [stacks instr] (crate-mover stacks instr))
(:stacks data) (:instrs data)))
#object[advent_of_clerk.day_05$run_instructions 0x4a945e6e "
advent_of_clerk.day_05$run_instructions@4a945e6e"
]
(defn top-crates [stacks]
(->> stacks
(sort-by first)
(map (comp first second))
(apply str)))
#object[advent_of_clerk.day_05$top_crates 0x1b0ff95b "
advent_of_clerk.day_05$top_crates@1b0ff95b"
]
(defn solve-1
[input]
(->> input
parse
(run-instructions cratemover9000)
top-crates))
#object[advent_of_clerk.day_05$solve_1 0x76dafd0e "
advent_of_clerk.day_05$solve_1@76dafd0e"
]

Studies

(def data (parse ex-input))
{:instrs ([1 2 1] [3 1 3] [2 2 1] [1 1 2]) :stacks {1 (\N \Z) 2 (\D \C \M) 3 (\P)}}
(cratemover9000 {1 (list \D \C \M) 2 (list \N \Z)}
[2 1 2])
{1 (\M) 2 (\C \D \N \Z)}
(def final-stack
(reduce (fn [stacks instr] (cratemover9000 stacks instr))
(:stacks data) (:instrs data)))
{1 (\C) 2 (\M) 3 (\Z \N \D \P)}
(apply str (map (comp first second) (sort-by first final-stack)))
"
CMZ"

Observations

  • when multiple crates are moved, their order will be reversed in the stack
  • in the input, all crates are the same amount of chars apart
  • I feel like parsing the input was the hardest part of the puzzle

TIL:

  • peek and pop behave differently on vectors vs lists and throw on some special sequences… I replaced them with first and rest instead, but still have to learn how and why they would be used in Clojure
  • could have used (s/split #"\n\n") instead of partition to split the input
  • you can use pipes in regex-pattern for re-seq to split a sequence into alternative parts
  • you can “transpose” across a seq of seqs by applying map to all of them, which would have helped with the initial stack input (a string is also a seq)

I really like this transpose-based parsing approach (adapted from @motform):

(let [[init _] (cstr/split (utils/load-input "day_05.txt") #"\n\n")
transpose (fn [matrix] (apply map vector matrix))
->stack (fn [xs] {(-> xs last str parse-long) (drop-last xs)})]
(->> init
cstr/split-lines
(apply map vector)
(map (partial remove #(#{\space \[ \]} %)))
(remove empty?)
(map ->stack)
(apply merge)))
{1 (\N \W \B) 2 (\B \M \D \T \P \S \Z \L) 3 (\R \W \Z \H \Q) 4 (\R \Z \J \V \D \W) 5 (\B \M \H \S) 6 (\B \P \V \H \J \N \G \L) 7 (\S \L \D \H \F \Z \Q \J) 8 (\B \Q \G \J \F \S \W) 9 (\J \D \C \S \M \W \Z)}

Part 2

(defn cratemover9001 [stacks [n from to]]
(-> stacks
(update to #(concat (take n (stacks from)) %))
(update from #(drop n %))))
#object[advent_of_clerk.day_05$cratemover9001 0x33113d8b "
advent_of_clerk.day_05$cratemover9001@33113d8b"
]
(defn solve-2
[input]
(->> input
parse
(run-instructions cratemover9001)
top-crates))
#object[advent_of_clerk.day_05$solve_2 0x5b1e62a9 "
advent_of_clerk.day_05$solve_2@5b1e62a9"
]

Studies

(cratemover9001 {1 (list \D \C \M) 2 (list \N \Z)}
[2 1 2])
{1 (\M) 2 (\D \C \N \Z)}

Observations

  • no reverse ordering of moved stacks here
  • of course, the first part could have been solved similarly without the reduce for iteration, but I like how it displays the slower one-by-one stack operation of the CrateMover9000 vs the fast all-at-once chunk moving of the CrateMover9001 :)
(comment
)
nil