From d82593903add24028e5780a0ecabf5733150b43f Mon Sep 17 00:00:00 2001 From: Rebecca Le Date: Mon, 16 Dec 2024 18:22:44 +0800 Subject: [PATCH] 2024 day 16, part 2 Feels like I'm doing some unnecessary stuff to figure out all of the paths non-exponentially but hey it's quick --- .gitattributes | 1 + README.md | 4 +- lib/y2024/README.md | 5 ++- lib/y2024/day16.ex | 80 +++++++++++++++++++++++++++++---------- test/y2024/day16_test.exs | 7 +++- 5 files changed, 72 insertions(+), 25 deletions(-) diff --git a/.gitattributes b/.gitattributes index 0ade7d7..412835a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,2 @@ lib/y*/input/day*.txt filter=git-crypt diff=git-crypt +test/y*/input/day*.txt filter=git-crypt diff=git-crypt diff --git a/README.md b/README.md index 89f8057..59df201 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,8 @@ My Elixir solutions for [Advent of Code](https://adventofcode.com/) (all years). -

449 stars

-

30 stars
+

450 stars

+

31 stars
44 stars
50 stars
46 stars
diff --git a/lib/y2024/README.md b/lib/y2024/README.md index 63669d3..54c917e 100644 --- a/lib/y2024/README.md +++ b/lib/y2024/README.md @@ -2,7 +2,7 @@ My Elixir solutions for [Advent of Code 2024](https://adventofcode.com/2024). -30 stars +31 stars ## Benchmarks @@ -43,5 +43,6 @@ day 13, part 1 1294.57 0.77 ms ±4.01% 0.76 ms 0. day 14, part 1 516.12 1.94 ms ±8.66% 1.85 ms 2.30 ms day 14, part 2 1.41 707.27 ms ±0.37% 707.76 ms 710.36 ms day 15, part 1 5.06 197.80 ms ±0.60% 197.59 ms 201.50 ms -day 16, part 1 13.98 71.55 ms ±6.08% 73.25 ms 80.54 ms +day 16, part 1 12.91 77.43 ms ±5.49% 77.14 ms 86.71 ms +day 16, part 2 13.07 76.51 ms ±5.00% 76.09 ms 85.69 ms ``` diff --git a/lib/y2024/day16.ex b/lib/y2024/day16.ex index 8932719..29977f3 100644 --- a/lib/y2024/day16.ex +++ b/lib/y2024/day16.ex @@ -4,24 +4,24 @@ defmodule Y2024.Day16 do alias Advent.PathGrid def part1(path_grid) do - from = Enum.find(path_grid.units, &(&1.identifier == "S")).position - to = Enum.find(path_grid.units, &(&1.identifier == "E")).position + do_parts(path_grid) |> elem(0) + end - {_path, score} = find_lowest_score(path_grid.graph, from, to) - score + def part2(path_grid) do + do_parts(path_grid) + |> elem(1) + |> MapSet.size() end - # @doc """ - # iex> Day16.part2("update or delete me") - # "update or delete me" - # """ - # def part2(input) do - # input - # end + defp do_parts(path_grid) do + from = Enum.find(path_grid.units, &(&1.identifier == "S")).position + to = Enum.find(path_grid.units, &(&1.identifier == "E")).position + find_lowest_scores(path_grid.graph, from, to) + end - defp find_lowest_score(graph, from, to) do + defp find_lowest_scores(graph, from, to) do queue = queue_next_states(PriorityQueue.new(), {from, :east, [{from, :east}], 0}, graph) - do_search(PriorityQueue.pop(queue), graph, to, MapSet.new([{from, :east}])) + do_search(PriorityQueue.pop(queue), graph, to, %{{from, :east} => {0, []}}) end defp queue_next_states(queue, {coord, facing, path, score}, graph) do @@ -48,13 +48,53 @@ defmodule Y2024.Day16 do defp do_search({{:value, {coord, facing, path, score}}, queue}, graph, to, seen) do if coord == to do - {path, score} + # We've found a best path! We might have had multiple branches that combined + # together to get to this point though + {score, get_all_paths(path, seen)} else - if MapSet.member?(seen, {coord, facing}) do - do_search(PriorityQueue.pop(queue), graph, to, seen) - else - queue = queue_next_states(queue, {coord, facing, path, score}, graph) - do_search(PriorityQueue.pop(queue), graph, to, MapSet.put(seen, {coord, facing})) + case Map.get(seen, {coord, facing}) do + {^score, old_path} -> + # We've found an alternate way of getting to this point + # We can cut this path (but record that it existed) + seen = Map.put(seen, {coord, facing}, {score, [path | old_path]}) + do_search(PriorityQueue.pop(queue), graph, to, seen) + + # We've seen a better way of getting to this point (skip) + {old_score, _old_path} when old_score < score -> + do_search(PriorityQueue.pop(queue), graph, to, seen) + + _ -> + # Either the first time we've seen this coordinate, or this is a better way + # of getting to it (actions are the same) + seen = Map.put(seen, {coord, facing}, {score, path}) + queue = queue_next_states(queue, {coord, facing, path, score}, graph) + do_search(PriorityQueue.pop(queue), graph, to, seen) + end + end + end + + # This feels rather unnecessary but I can't think of a better way to do it + # A BFS over all of the points in the path to build all of the ways we got there + defp get_all_paths(path, seen) do + do_get_all_paths(path, [], MapSet.new(), seen) + end + + defp do_get_all_paths([], [], path, _seen), do: path + + defp do_get_all_paths([], next, path, seen) do + do_get_all_paths(List.flatten(next), [], path, seen) + end + + defp do_get_all_paths([{coord, _} = one | rest], next, path, seen) do + if MapSet.member?(path, coord) do + do_get_all_paths(rest, next, path, seen) + else + case Map.fetch(seen, one) do + :error -> + do_get_all_paths(rest, next, MapSet.put(path, coord), seen) + + {:ok, {_score, points}} -> + do_get_all_paths(rest, [points | next], MapSet.put(path, coord), seen) end end end @@ -64,5 +104,5 @@ defmodule Y2024.Day16 do end def part1_verify, do: input() |> parse_input() |> part1() - # def part2_verify, do: input() |> parse_input() |> part2() + def part2_verify, do: input() |> parse_input() |> part2() end diff --git a/test/y2024/day16_test.exs b/test/y2024/day16_test.exs index 656a9aa..f5abc70 100644 --- a/test/y2024/day16_test.exs +++ b/test/y2024/day16_test.exs @@ -8,8 +8,13 @@ defmodule Y2024.Day16Test do assert test_data("sample2") |> Day16.parse_input() |> Day16.part1() == 11048 end + test "part2" do + assert test_data("sample1") |> Day16.parse_input() |> Day16.part2() == 45 + assert test_data("sample2") |> Day16.parse_input() |> Day16.part2() == 64 + end + test "verification, part 1", do: assert(Day16.part1_verify() == 91464) - # test "verification, part 2", do: assert(Day16.part2_verify() == "update or delete me") + test "verification, part 2", do: assert(Day16.part2_verify() == 494) def test_data(name), do: File.read!("test/y2024/input/#{name}.txt") end