Skip to content

Commit

Permalink
day 5
Browse files Browse the repository at this point in the history
  • Loading branch information
narimiran committed Dec 5, 2024
1 parent 083e71d commit 6f70216
Show file tree
Hide file tree
Showing 5 changed files with 1,675 additions and 1 deletion.
282 changes: 282 additions & 0 deletions clojure/day05.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,282 @@
^{:nextjournal.clerk/visibility {:code :hide :result :hide}}
(ns day05
{:nextjournal.clerk/auto-expand-results? true
:nextjournal.clerk/toc :collapsed}
(:require
aoc))


;; # Day 5: Print Queue
;;
;; Were at the _North Pole printing department_ and there's one
;; _very familiar printer_!
;; We haven't seen it in 7 years, and now we need to fix it again.
;;
;; > The Elf has for you both the page ordering rules and
;; > the pages to produce in each update
;;
;; It looks like this:
;;
(def example "47|53
97|13
97|61
97|47
75|29
61|13
75|53
29|13
97|29
53|29
61|53
97|53
61|29
47|13
75|47
97|75
47|61
75|61
47|29
75|13
53|13
75,47,61,53,29
97,61,53,29,13
75,29,13
75,97,47,61,53
61,13,29
97,13,75,29,47")





;; ## Input parsing
;;
;; We have two separate groups of numbers, separated by a blank line.
;; Another [AoC helper](./aoc) to the rescue: `aoc/parse-paragraphs`,
;; which separates each group in it's own list.\
;; For each group, we extract integers from each line.
;;
(defn parse-data [input]
(aoc/parse-paragraphs input :ints))

(def example-data (parse-data example))
(def data (parse-data (aoc/read-input 5)))





;; ## Dynamic rules
;;
;; We will have to check if the numbers follow the rules in multiple functions.
;; This means we'll have to either pass the rules as an argument to each
;; function, or have the rules defined as a global variable.
;;
;; The former idea will work fine, but it makes some function invocations
;; uglier than they would be if they only had a single argument.
;; (And we're not here to be content with ugly code!)\
;; The latter idea, with a global variable, will not work because
;; we're always solving both for the example input and the real one.
;; We cannot have a single global variable that works in both cases.
;;
;; Enter dynamic vars!
;;
;; This will allow us to use specific rules, depending on the input we're
;; using.\
;; We will `bind` the rules in our main functions and that value will be
;; "seen" by every function we call from there.\
;; See the documentation of the [`binding` macro](https://clojuredocs.org/clojure.core/binding)
;; to see some examples how this is used.
;;
;; We will mark `rules` as a `^:dynamic` variable, and by convention we need
;; to put "earmuffs" around it:
;;
(def ^:dynamic *rules* #{})





;; ## Part 1
;;
;; It seems that inputs are created in such a way that for every pair of
;; numbers in the updates list, there is a rule which says which one goes
;; before the other.
;; (More about that in a later section.)
;;
;; This means it is enough to check if all adjacent numbers
;; (we'll use `(partition 2 1 ...)` for that, like we did in [Day 2](./day02))
;; in a line follow the rules:
;;
(defn valid-line? [line]
(every? *rules* (partition 2 1 line)))


;; > Determine which updates are already in the correct order.
;; > What do you get if you add up the middle page number from those
;; > correctly-ordered updates?
;;
;; From the list of updates, we will `filter` the lines which are in the
;; correct order, and for each of those we need to `sum` the numbers that are
;; in the middle of each line:
;;
(defn middle-page [line]
(line (quot (count line) 2)))

(defn part-1 [[rules updates]]
(binding [*rules* (set rules)]
(->> updates
(filter valid-line?)
(aoc/sum-map middle-page))))

(part-1 example-data)
(part-1 data)


;; As you can see in the code above, each time we call the `part-1` function,
;; we bind the `*rules*` variable to have a new value, depending on the
;; argument we're passing in.\
;; When we call the `valid-line?` function, it will see this new value for
;; `*rules*` and we'll successfully `filter` the correctly-ordered updates.






;; ## Part 2
;;
;; Ok, we've successfully discovered the correctly-ordered updates, but now:
;;
;; > For each of the incorrectly-ordered updates,
;; > use the page ordering rules to put the page numbers in the right order.
;;
;; We can still use the `valid-line?` function as a predicate, but this time
;; we will [`remove`](https://clojuredocs.org/clojure.core/remove) the lines
;; which are in the correct order.
;;
;; For the lines that remain, we need to
;; [`sort`](https://clojuredocs.org/clojure.core/sort) them so they follow
;; the rules, and then take the middle number.\
;; We will write a custom function to use with `sort`: if two numbers are
;; in the `*rules*`, they are in the correct order (the first one is "lower"
;; than the second one):
;;
(defn sort-line [line]
(->> line
(sort (fn [a b]
(if (*rules* [a b]) -1 1)))
vec))


;; The result of a `sort` is a sequence, and we need to convert it into
;; a vector to be able to take the middle element with the
;; `middle-page` function.
;;
;; The complete part 2 is now:
;;
(defn part-2 [[rules updates]]
(binding [*rules* (set rules)]
(->> updates
(remove valid-line?)
(map sort-line)
(aoc/sum-map middle-page))))

(part-2 example-data)
(part-2 data)






;; ## Both parts at once
;;
;; We can see that the `part-1` and `part-2` functions are very similar.
;; No need to repeat ourselves, we can have a single function which solves
;; both parts.
;;
;; First we'll split the updates in valid and invalid ones.
;; We will use the [`group-by` function](https://clojuredocs.org/clojure.core/group-by).
;; This will give us a hashmap with two keys: `true` (for the updates which
;; satisfy the `valid-line?` predicate) and `false` (for the invalid updates),
;; so we'll immediately unpack those, since we're only interested in their values.
;;
;; After that, we'll have to sort the invalid ones, and then for each group
;; we will calculate the sum of middle pages.
;;
(defn solve [[rules updates]]
(binding [*rules* (set rules)]
(let [{valid true
invalid false} (group-by valid-line? updates)
sorted-invalid (map sort-line invalid)]
(mapv #(aoc/sum-map middle-page %) [valid sorted-invalid]))))

(solve example-data)
(solve data)




;; ## Nice inputs
;;
;; I've claimed in the Part 1 section that we have nice inputs which simplify
;; sorting.
;; Let's briefly see what's going on.
;;
;; For each input, we have some number of rules.\
;; How many unique numbers are there in the updates?
;; Is there any connection between the two?
;;
;; Let's explore that with our real input:
;;
(defn find-unique-numbers [[_ updates]]
(-> updates
flatten
set
count))

(def unique-numbers (find-unique-numbers data))


;; We can have `n * (n - 1) / 2` different combinations of `n` different numbers.
;; For our case that is:
(/ (* unique-numbers (dec unique-numbers)) 2)

;; And how many rules do we have?
(count (first data))

;; There you go!
;; We have a rule for every possible combination of two numbers, that's why
;; we could solve it this way.







;; ## Conclusion
;;
;; Initially this task looked way harder than it really was in the end.
;; (Well, most of them do, when you open them at 6 a.m.)
;;
;; Fortunately, the inputs are created in such way that makes sorting the
;; updates easy.
;;
;; Today's highlights:
;; - `^:dynamic`: declare a dynamic variable
;; - `binding`: bind a value for a dynamic variable
;; - `group-by`: split a collection into groups, based on a predicate








^{:nextjournal.clerk/visibility {:code :hide :result :hide}}
(defn -main [input]
(solve (parse-data input)))
3 changes: 2 additions & 1 deletion clojure/tests/solutions_tests.clj
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
(ns solutions-tests
(:require
day01 day02 day03 day04 ;day05
day01 day02 day03 day04 day05
;; day06 day07 day08 day09 day10
;; day11 day12 day13 day14 day15
;; day16 day17 day18 day19 day20
Expand All @@ -27,6 +27,7 @@
(check-day 2 [2 4] [334 400])
(check-day 3 [161 48] [167090022 89823704])
(check-day 4 [18 9] [2534 1866])
(check-day 5 [143 123] [5091 4681])


(let [summary (run-tests)]
Expand Down
1 change: 1 addition & 0 deletions index.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,5 @@ Task | Notebook | Comment
[Day 2: Red-Nosed Reports](https://adventofcode.com/2024/day/2) | [day02.clj](clojure/day02) | The Problem Dampener would be useful for AoC!
[Day 3: Mull It Over](https://adventofcode.com/2024/day/3) | [day03.clj](clojure/day03) | Regex and chill.
[Day 4: Ceres Search](https://adventofcode.com/2024/day/4) | [day04.clj](clojure/day04) | No Más!
[Day 5: Ceres Search](https://adventofcode.com/2024/day/5) | [day05.clj](clojure/day05) | Easier than it looked initially.

Loading

0 comments on commit 6f70216

Please sign in to comment.