aboutsummaryrefslogtreecommitdiff
path: root/racket/aoc2021/day-15/day-15.livemd
diff options
context:
space:
mode:
Diffstat (limited to 'racket/aoc2021/day-15/day-15.livemd')
-rw-r--r--racket/aoc2021/day-15/day-15.livemd287
1 files changed, 287 insertions, 0 deletions
diff --git a/racket/aoc2021/day-15/day-15.livemd b/racket/aoc2021/day-15/day-15.livemd
new file mode 100644
index 0000000..2495c32
--- /dev/null
+++ b/racket/aoc2021/day-15/day-15.livemd
@@ -0,0 +1,287 @@
+<!-- vim: set syntax=markdown: -->
+<!-- livebook:{"persist_outputs":true} -->
+
+# Advent of Code 2021, Day 15
+
+## Short summary
+
+**Parts 1 and 2.** Find the least "risky" path through a grid of nodes,
+where each node has a "risk" cost to enter. Part 1's grid is the given data,
+while Part 2's is the data after undergoing a transformation.
+Only orthogonal travel is possible.
+
+## Setup
+
+Using the `libgraph` library to build the graph
+and use its implementation of the Dijkstra algorithm:
+
+```elixir
+Mix.install([
+ {:libgraph, github: "bitwalker/libgraph"}
+])
+```
+
+```output
+* Getting libgraph (https://github.com/bitwalker/libgraph.git)
+remote: Enumerating objects: 783, done.
+remote: Counting objects: 100% (64/64), done.
+remote: Compressing objects: 100% (52/52), done.
+remote: Total 783 (delta 27), reused 24 (delta 10), pack-reused 719
+origin/HEAD set to main
+==> libgraph
+Compiling 14 files (.ex)
+Generated libgraph app
+```
+
+```output
+:ok
+```
+
+```elixir
+floor =
+ with {:ok, data} <- File.read("./2021/day-15/input") do
+ data
+ |> String.split()
+ |> Enum.map(fn xs ->
+ String.graphemes(xs)
+ |> Enum.map(&String.to_integer/1)
+ end)
+ end
+```
+
+```output
+[
+ [4, 5, 5, 2, 2, 8, 5, 9, 8, 9, 4, 4, 1, 1, 2, 4, 7, 1, 9, 7, 9, 8, 4, 6, 5, 8, 2, 5, 7, 7, 3, 3,
+ 1, 8, 2, 5, 2, 2, 6, 9, 4, 2, 2, 6, 2, 5, 7, 3, 1, ...],
+ [8, 3, 1, 1, 8, 2, 6, 9, 1, 7, 6, 7, 7, 2, 9, 5, 5, 6, 1, 3, 9, 5, 7, 1, 6, 2, 4, 5, 4, 2, 6, 1,
+ 8, 4, 2, 1, 2, 4, 7, 1, 3, 1, 1, 2, 1, 4, 8, 5, ...],
+ [3, 1, 3, 4, 1, 5, 5, 5, 2, 8, 2, 3, 1, 2, 9, 6, 3, 1, 3, 2, 9, 2, 9, 5, 1, 6, 8, 9, 3, 7, 1, 6,
+ 7, 1, 6, 1, 1, 3, 5, 5, 1, 9, 9, 4, 2, 9, 3, ...],
+ [7, 1, 3, 4, 1, 5, 9, 6, 8, 1, 5, 3, 3, 3, 3, 1, 1, 7, 2, 9, 9, 1, 9, 3, 2, 8, 1, 9, 1, 2, 8, 7,
+ 1, 8, 1, 3, 1, 1, 2, 1, 4, 2, 8, 7, 1, 5, ...],
+ [3, 2, 9, 1, 6, 2, 2, 1, 2, 1, 8, 9, 3, 8, 2, 1, 2, 5, 8, 2, 1, 5, 1, 1, 1, 5, 3, 1, 6, 1, 4, 2,
+ 3, 3, 9, 9, 9, 6, 6, 1, 6, 4, 9, 2, 7, ...],
+ [9, 1, 1, 6, 1, 9, 7, 3, 1, 9, 9, 9, 2, 2, 2, 3, 7, 8, 4, 9, 7, 3, 7, 4, 9, 1, 5, 9, 9, 1, 2, 9,
+ 1, 8, 9, 4, 8, 5, 1, 4, 8, 5, 5, 2, ...],
+ [4, 9, 6, 8, 4, 3, 9, 1, 1, 4, 1, 3, 5, 9, 9, 6, 9, 2, 3, 9, 3, 9, 6, 2, 1, 5, 2, 1, 2, 2, 7, 4,
+ 3, 8, 6, 1, 6, 4, 3, 2, 2, 6, 2, ...],
+ [6, 2, 3, 9, 2, 1, 9, 7, 8, 5, 2, 3, 4, 6, 7, 5, 3, 9, 1, 2, 8, 1, 6, 4, 1, 1, 1, 4, 1, 4, 8, 2,
+ 2, 5, 9, 4, 2, 6, 2, 2, 1, 9, ...],
+ [1, 9, 3, 9, 2, 7, 3, 1, 5, 2, 1, 1, 4, 1, 6, 5, 8, 1, 5, 6, 2, 1, 8, 9, 8, 1, 3, 1, 7, 4, 1, 6,
+ 2, 5, 1, 4, 2, 2, 7, 3, 7, ...],
+ [3, 8, 1, 6, 2, 9, 5, 1, 2, 1, 2, 1, 1, 7, 1, 3, 9, 1, 9, 9, 1, 8, 1, 2, 9, 1, 4, 1, 5, 2, 1, 7,
+ 4, 8, 3, 1, 5, 5, 7, 9, ...],
+ [2, 8, 5, 2, 1, 7, 4, 3, 9, 2, 1, 7, 5, 2, 1, 7, 8, 9, 1, 2, 9, 2, 7, 6, 9, 2, 9, 7, 3, 2, 9, 7,
+ 3, 4, 1, 4, 1, 5, 2, ...],
+ [8, 1, 8, 3, 4, 1, 1, 9, 8, 1, 1, 3, 6, 7, 1, 8, 4, 2, 8, 8, 2, 3, 1, 2, 7, 6, 8, 1, 4, 1, 1, 2,
+ 4, 6, 5, 2, 8, 2, ...],
+ [3, 3, 3, 8, 4, 1, 8, 9, 5, 8, 8, 1, 8, 2, 2, 9, 3, 3, 1, 3, 9, 9, 5, 1, 3, 6, 9, 1, 9, 2, 1, 9,
+ 2, 1, 8, 6, 9, ...],
+ [6, 1, 1, 8, 5, 8, 2, 7, 6, 5, 3, 7, 5, 5, 4, 1, 1, 4, 4, 4, 9, 1, 5, 1, 9, 8, 7, 1, 1, 2, 3, 1,
+ 2, 1, 1, 7, ...],
+ [1, 4, 6, 9, 2, 5, 7, 4, 1, 8, 3, 1, 3, 2, 1, 8, 9, 5, 1, 2, 1, 1, 5, 1, 1, 5, 9, 1, 2, 2, 4, 2,
+ 8, 1, 8, ...],
+ [1, 4, 2, 2, 1, 1, 8, 4, 1, 9, 3, 1, 9, 5, 1, 5, 4, 9, 6, 1, 8, 9, 3, 9, 1, 1, 5, 8, 5, 1, 6, 4,
+ 6, 2, ...],
+ [8, 1, 6, 2, 5, 4, 6, 1, 3, 9, 9, 5, 2, 1, 3, 6, 1, 8, 9, 9, 1, 1, 1, 7, 7, 5, 3, 3, 3, 5, 7, 8,
+ 2, ...],
+ [1, 4, 8, 8, 2, 3, 9, 6, 1, 1, 4, 6, 1, 4, 9, 1, 9, 7, 3, 8, 6, 5, 1, 8, 5, 2, 4, 2, 9, 5, 9, 5,
+ ...],
+ [2, 2, 1, 8, 9, 1, 7, 6, 6, 1, 8, 6, 8, 1, 3, 3, 1, 4, 1, 2, 4, 3, 8, 1, 9, 9, 6, 2, 4, 1, 3, ...],
+ [5, 7, 3, 4, 3, 2, 6, 7, 3, 2, 8, 8, 4, 7, 9, 2, 4, 2, 8, 1, 9, 1, 2, 4, 1, 1, 2, 1, 1, 9, ...],
+ [1, 5, 1, 1, 9, 3, 5, 1, 1, 1, 8, 6, 1, 1, 2, 1, 9, 3, 5, 7, 4, 1, 7, 6, 8, 3, 3, 8, 9, ...],
+ [9, 6, 9, 1, 5, 6, 2, 1, 8, 3, 5, 1, 3, 3, 8, 8, 2, 2, 1, 7, 2, 6, 4, 9, 3, 8, 2, 1, ...],
+ [2, 7, 2, 9, 2, 3, 7, 5, 3, 2, 9, 5, 9, 4, 4, 4, 7, 3, 3, 2, 2, 5, 4, 3, 5, 9, 9, ...],
+ [7, 3, 1, 2, 5, 5, 9, 1, 8, 1, 3, 2, 3, 7, 4, 3, 9, 3, 8, 2, 2, 1, 2, 1, 2, 2, ...],
+ [9, 4, 1, 5, 7, 4, 7, 7, 3, 8, 3, 4, 6, 1, 4, 1, 6, 9, 1, 4, 1, 8, 8, 2, 5, ...],
+ [1, 7, 1, 6, 2, 6, 3, 1, 2, 1, 1, 1, 5, 6, 3, 1, 1, 4, 1, 7, 8, 4, 2, 5, ...],
+ [2, 1, 1, 3, 4, 1, 4, 9, 1, 1, 7, 3, 1, 1, 6, 4, 9, 6, 2, 3, 4, 2, 2, ...],
+ [5, 6, 1, 1, 9, 1, 2, 3, 3, 4, 9, 2, 7, 2, 2, 1, 2, 5, 2, 7, 1, 1, ...],
+ [1, 2, 3, 2, 3, 1, 3, 2, 7, 1, 1, 1, 3, 5, 5, 4, 1, 8, 9, 9, 5, ...],
+ [2, 9, 2, 8, 1, 2, 3, 1, 2, 1, 1, 9, 1, 2, 6, 7, 7, 2, 1, 3, ...],
+ [3, 7, 3, 8, 1, 5, 2, 5, 4, 4, 6, 2, 5, 5, 1, 6, 9, 1, 6, ...],
+ [4, 9, 7, 8, 9, 3, 2, 2, 3, 7, 3, 1, 2, 6, 1, 5, 4, 5, ...],
+ [3, 9, 1, 2, 8, 9, 9, 1, 8, 2, 7, 9, 8, 9, 1, 6, 6, ...],
+ [1, 1, 9, 2, 9, 2, 7, 4, 2, 1, 3, 7, 2, 5, 8, 4, ...],
+ [3, 9, 1, 1, 1, 8, 5, 2, 1, 5, 5, 8, 2, 4, 3, ...],
+ [2, 9, 5, 3, 2, 1, 6, 7, 2, 2, 8, 1, 5, 2, ...],
+ [1, 1, 9, 8, 3, 8, 2, 7, 6, 7, 2, 1, 8, ...],
+ [2, 7, 2, 1, 8, 8, 4, 4, 1, 2, 9, 4, ...],
+ [3, 1, 1, 5, 1, 1, 8, 2, 2, 6, 2, ...],
+ [1, 4, 6, 6, 1, 3, 8, 1, 5, 2, ...],
+ [9, 1, 2, 2, 1, 3, 4, 4, 5, ...],
+ [6, 6, 1, 9, 4, 1, 3, 2, ...],
+ [1, 5, 9, 1, 8, 4, 5, ...],
+ [9, 1, 4, 5, 6, 7, ...],
+ [1, 7, 5, 6, 3, ...],
+ [7, 1, 1, 5, ...],
+ [1, 5, 9, ...],
+ [1, 5, ...],
+ [2, ...],
+ [...],
+ ...
+]
+```
+
+Give each node a label based on its coordinates:
+
+```elixir
+floor_nodes =
+ for {row, i} <- Enum.with_index(floor),
+ {col, j} <- Enum.with_index(row),
+ into: %{} do
+ {{i, j}, col}
+ end
+```
+
+```output
+%{
+ {76, 13} => 1,
+ {37, 47} => 2,
+ {65, 63} => 5,
+ {38, 2} => 1,
+ {1, 26} => 4,
+ {83, 76} => 2,
+ {32, 15} => 6,
+ {89, 14} => 1,
+ {35, 30} => 7,
+ {37, 53} => 7,
+ {4, 5} => 2,
+ {8, 50} => 7,
+ {78, 98} => 7,
+ {95, 56} => 7,
+ {74, 12} => 9,
+ {11, 39} => 2,
+ {65, 43} => 4,
+ {22, 38} => 1,
+ {14, 86} => 4,
+ {20, 41} => 1,
+ {29, 25} => 1,
+ {86, 10} => 1,
+ {83, 36} => 3,
+ {29, 26} => 3,
+ {47, 27} => 9,
+ {4, 81} => 3,
+ {31, 42} => 1,
+ {9, 34} => 3,
+ {90, 0} => 3,
+ {67, 98} => 1,
+ {13, 85} => 1,
+ {63, 81} => 4,
+ {82, 60} => 4,
+ {47, 38} => 1,
+ {15, 92} => 1,
+ {58, 58} => 1,
+ {20, 3} => 1,
+ {61, 95} => 7,
+ {23, 67} => 4,
+ {78, 75} => 1,
+ {79, 17} => 2,
+ {75, 0} => 7,
+ {16, 73} => 2,
+ {76, 2} => 8,
+ {58, 84} => 1,
+ {58, 33} => 7,
+ {47, 44} => 2,
+ {54, 31} => 6,
+ {13, ...} => 1,
+ {...} => 9,
+ ...
+}
+```
+
+We can travel in the four cardinal directions from each node, so we need
+a function to identify a node's neighbors:
+
+```elixir
+neighbors = fn {i, j}, nodes ->
+ [{i + 1, j}, {i - 1, j}, {i, j + 1}, {i, j - 1}]
+ |> Enum.filter(&Map.has_key?(nodes, &1))
+end
+
+[neighbors.({0, 0}, floor_nodes), neighbors.({50, 50}, floor_nodes)]
+```
+
+```output
+[[{1, 0}, {0, 1}], [{51, 50}, {49, 50}, {50, 51}, {50, 49}]]
+```
+
+## Part 1
+
+Now we fold all the edges into a `Graph`:
+
+```elixir
+make_graph = fn nodes ->
+ for {coord, _} <- nodes,
+ neighbor <- neighbors.(coord, nodes),
+ risk = Map.get(nodes, neighbor),
+ reduce: Graph.new(vertex_identifier: fn v -> v end) do
+ acc -> Graph.add_edge(acc, coord, neighbor, weight: risk)
+ end
+end
+
+floor_graph = make_graph.(floor_nodes)
+```
+
+```output
+#Graph<type: directed, num_vertices: 10000, num_edges: 39600>
+```
+
+Now we just need to use Dijkstra's algorithm to find the best path from the upper left to the lower right,
+and use the map of each node's risk value to sum up the total risk for the path.
+
+```elixir
+get_lowest_risk_path = fn nodes ->
+ with {{min_c, _}, {max_c, _}} <- Enum.min_max_by(nodes, fn {{i, j}, _} -> i + j end) do
+ nodes
+ |> then(make_graph)
+ |> Graph.dijkstra(min_c, max_c)
+ |> tl()
+ |> Enum.reduce(0, fn coord, sum -> sum + Map.get(nodes, coord) end)
+ end
+end
+
+get_lowest_risk_path.(floor_nodes)
+```
+
+```output
+403
+```
+
+## Part 2
+
+The process for Part 2 will be similar; the only difference
+is that the graph is five times bigger after the transform.
+
+If the transformed risk is greater than 9, it rolls over to 1, so a
+`Stream.cycle` can be used to easily get the rolled-over values.
+
+```elixir
+expanded_floor_nodes =
+ with {{max_i, max_j}, _} <- Enum.max_by(floor_nodes, fn {{i, j}, _} -> i + j end),
+ nine_cycle = Stream.cycle(1..9) do
+ for {{i, j}, risk} <- floor_nodes,
+ x <- 0..4,
+ y <- 0..4,
+ into: %{} do
+ {{i + x * (max_i + 1), j + y * (max_j + 1)}, Enum.at(nine_cycle, risk - 1 + x + y)}
+ end
+ end
+
+Enum.count(expanded_floor_nodes)
+```
+
+```output
+250000
+```
+
+We repeat the same steps as before: building the graph,
+using Dijkstra's algorithm and summing up the risks along the best path.
+
+```elixir
+get_lowest_risk_path.(expanded_floor_nodes)
+```
+
+```output
+2840
+```