Skip to content

Commit

Permalink
day 16
Browse files Browse the repository at this point in the history
  • Loading branch information
narimiran committed Dec 18, 2024
1 parent 450175f commit 2415fc5
Show file tree
Hide file tree
Showing 5 changed files with 393 additions and 1 deletion.
234 changes: 234 additions & 0 deletions clojure/day16.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
^{:nextjournal.clerk/visibility {:code :hide :result :hide}}
(ns day16
{:nextjournal.clerk/auto-expand-results? true
:nextjournal.clerk/toc :collapsed}
(:require
aoc
[nextjournal.clerk :as clerk]))


;; # Day 16: Reindeer Maze
;;
;; Unlike human Olympic, Reindeer Olympics are happening every 9 years!?
;;
;; We are given a map like this:
;;
(def example "###############
#.......#....E#
#.#.###.#.###.#
#.....#.#...#.#
#.###.#####.#.#
#.#.#.......#.#
#.#.#####.###.#
#...........#.#
###.#.#####.#.#
#...#.....#.#.#
#.#.#.###.#.#.#
#.....#...#.#.#
#.###.#.#.#.#.#
#S..#.....#...#
###############")

;; The Reindeer start at `S`, facing east, and need to come to end, marked `E`.
;; They can move forward, costing them `1` point, or they can stay where
;; they are and rotate 90 degrees to the left or to the right, costing
;; them `1000` points.
;;
;; A graph with unequal weights between nodes.
;; Time to bring out Dijkstra.


;; ## Input parsing
;;
;; We need to extract the positions of the walls, the start and the end.
;; There is more performant way than doing it like this (going through the
;; input multiple times), but — as I already said previously — this year
;; is not about the performance.
;;
(defn parse-data [input]
(let [lines (aoc/parse-lines input)
walls (aoc/grid->point-set lines #{\#})
start (first (aoc/grid->point-set lines #{\S}))
end (first (aoc/grid->point-set lines #{\E}))]
[walls start end]))

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




;; ## Part 1
;;
;; For this part, we need to find the shortest path between the start and
;; the end.
;;
;; At each point we have three `neighbours`:
;; - we continue forward keeping the same direction
;; - we stay at the current point and turn left
;; - we stay at the current point and turn right
;;
(defn neighbours [[pt [dx dy :as delta]]]
[[(aoc/pt+ pt delta) delta]
[pt [dy (- dx)]] ; left
[pt [(- dy) dx]]]) ; right



;; Once again, I'm dogfeeding my graph traversal helper.
;;
;; Our starting point is the coordinate of `start` plus the direction
;; towards east (`[pt dir]`).
;; We are done when we reach the `end`, in any direction.
;;
;; Since the `walls` only have coordinates (and not directions), we need
;; to specify the `:nb-cond` function to check if a coordinate of a point is
;; (not) part of the walls.
;;
;; Making a turn costs more than continuing straight, so we need to
;; define the `:cost-fn` function for each movement we make:
;;
(defn traverse [walls start start-dir end]
(aoc/dijkstra {:start [start start-dir]
:end-cond (fn [[pt _]] (= end pt))
:walls walls
:nb-cond (fn [[nb _]] (not (walls nb)))
:nb-func neighbours
:cost-fn (fn [[_ dir1] [_ dir2]]
(if (= dir1 dir2) 1 1000))}))



;; The Reindeer are now ready to make a run:
;;
(defn olympics [[walls start end]]
(traverse walls start [1 0] end))


;; In Part 1 we're interested only in the minimum "score", i.e. the least
;; amount of points needed to run :
;;
(defn part-1 [data]
(-> data
olympics
:steps))

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




;; ## Part 2
;;
;; In Part 1 we found _a_ best path, but it is not the only one.
;; Here we need to find _all_ points that are on _all_ best paths.
;;
;; There are several ways to do that.\
;; We could continue running the original algorithm even when we find the end
;; for the first time, and every time we reach the end again check if the
;; score is the same as the best one, and then add points in that path
;; to an accumulator.
;; But due to the large size of the grid, this is very slow.
;;
;; Another approach is to extract the costs for each point (more precisely:
;; the `[pt dir]` pair) we encountered initially.
;; Then we run backwards, from `end` to `start` and also extract the costs
;; of each point we visit.
;;
;; From Part 1 we know the `cost(start, end)` of the best run.
;; Now we check for each point visited if `cost(start, pt) + cost(pt, end) =
;; cost(start, end)`.\
;; (Since we're going backwards, the directions will be opposite, we need
;; to reverse them: `(mapv - d)`)
;;
(defn on-best-path? [best-score fwd-costs bkw-costs]
(keep (fn [[[pt d] fwd-cost]]
(when-let [bkw-cost (bkw-costs [pt (mapv - d)])]
(when (= best-score (+ fwd-cost bkw-cost))
pt)))
fwd-costs))



;; Since the `end` is in the top-right corner, there's no need to check
;; east and north directions.
;; For other two directions, we `traverse` form `end` to `start`.
;; We need to take the `:costs` of all visited points, and we add all points
;; that are `on-best-path?` to the `best-spots`.
;; In the end, we're interested in the `count` of such points.
;;
(defn part-2 [[walls start end :as data]]
(let [fwd-results (olympics data)
best-result (:steps fwd-results)
fwd-costs (:costs fwd-results)]
(-> (reduce (fn [best-spots init-dir]
(->> (traverse walls end init-dir start)
:costs
(on-best-path? best-result fwd-costs)
(into best-spots)))
#{}
[[0 1] [-1 0]]) ; south and west
count)))


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



;; ## Visualization
;;
;; Let's see the best path!
;;
(let [walls (first data)
path (map first (:path (olympics data)))
axes-common {:ticks ""
:showticklabels false
:showgrid false
:zeroline false}]
(clerk/plotly
{:config {:displayModeBar false
:displayLogo false}
:data [{:x (map first walls)
:y (map second walls)
:mode :markers
:marker {:symbol :square
:size 4
:color "777"}}
{:x (map first path)
:y (map second path)}]
:layout {:xaxis axes-common
:yaxis (merge axes-common {:autorange :reversed})
:margin {:l 0 :r 0 :t 0 :b 0}
:showlegend false}}))



;; ## Conclusion
;;
;; One of the rare times that Dijkstra's algorithm is needed.
;; Usually the BFS is enough.
;;
;; For Part 2 we could write a more performant solution where we immediately
;; check (when choosing valid neighbours) if a point is on the best path,
;; and discard it if it isn't.
;; But I'm sticking with my path traversal hepler :)
;;
;; Today's highlights:
;; - `(mapv - coll)`: change sign of all numbers in the `coll`
;; - `keep`: keep only non-nil results of a predicate function
;; - `when-let`: evaluate a test and, if true, bind the result








^{:nextjournal.clerk/visibility {:code :hide :result :hide}}
(defn -main [input]
(let [data (parse-data input)]
[(part-1 data)
(part-2 data)]))
3 changes: 2 additions & 1 deletion clojure/tests/solutions_tests.clj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
day01 day02 day03 day04 day05
day06 day07 day08 day09 day10
day11 day12 day13 day14 day15
;; day16 day17 day18 day19 day20
day16 ;day17 day18 day19 day20
;; day21 day22 day23 day24 day25
[clojure.test :refer [deftest is run-tests successful?]]))

Expand Down Expand Up @@ -38,6 +38,7 @@
(check-day 13 [480 875318608908] [27105 101726882250942])
(check-day 14 nil [226236192 8168])
(check-day 15 10092 1509863)
(check-day 16 [7036 45] [79404 451])


(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 @@ -68,4 +68,5 @@ Task | Notebook
[Day 13: Claw Contraption](https://adventofcode.com/2024/day/13) | [day13.clj](clojure/day13) | | Kosmo Cramer!
[Day 14: Restroom Redoubt](https://adventofcode.com/2024/day/14) | [day14.clj](clojure/day14) | viz, anim | Christmas egg: Easter tree.
[Day 15: Warehouse Woes](https://adventofcode.com/2024/day/15) | [day15.clj](clojure/day15) | anim | Sokoban.
[Day 16: Reindeer Maze](https://adventofcode.com/2024/day/16) | [day16.clj](clojure/day16) | viz | Dijkstra.

Loading

0 comments on commit 2415fc5

Please sign in to comment.