|
| 1 | +defmodule AdventOfCode.Y2019.Day18 do |
| 2 | + @moduledoc """ |
| 3 | + --- Day 18: Many-Worlds Interpretation --- |
| 4 | + Shortest path to collect all keys in a maze. |
| 5 | + """ |
| 6 | + import Bitwise |
| 7 | + alias AdventOfCode.Helpers.{InputReader, Transformers} |
| 8 | + alias Yog.Pathfinding.Dijkstra |
| 9 | + |
| 10 | + def input, do: InputReader.read_from_file(2019, 18) |
| 11 | + |
| 12 | + def run(input \\ input()) do |
| 13 | + lines = Transformers.lines(input) |
| 14 | + |
| 15 | + p1 = solve_part_1(lines) |
| 16 | + p2 = solve_part_2(lines) |
| 17 | + |
| 18 | + {p1, p2} |
| 19 | + end |
| 20 | + |
| 21 | + # Part 1: Single robot |
| 22 | + defp solve_part_1(lines) do |
| 23 | + grid = parse_grid(lines) |
| 24 | + pois = find_pois(grid, ["@"]) |
| 25 | + all_keys_mask = calculate_keys_mask(pois) |
| 26 | + adj = build_poi_graph(grid, pois) |
| 27 | + |
| 28 | + start_id = "@" |
| 29 | + |
| 30 | + Dijkstra.implicit_dijkstra( |
| 31 | + from: {start_id, 0}, |
| 32 | + successors_with_cost: fn {at, collected} -> |
| 33 | + edges = Map.get(adj, at, []) |
| 34 | + |
| 35 | + edges |
| 36 | + |> Enum.filter(fn edge -> |
| 37 | + band(collected, edge.required) == edge.required and |
| 38 | + band(collected, key_bit(edge.to)) == 0 |
| 39 | + end) |
| 40 | + |> Enum.map(fn edge -> |
| 41 | + new_collected = bor(collected, key_bit(edge.to)) |
| 42 | + {{edge.to, new_collected}, edge.dist} |
| 43 | + end) |
| 44 | + end, |
| 45 | + is_goal: fn {_, collected} -> collected == all_keys_mask end, |
| 46 | + zero: 0, |
| 47 | + add: &Kernel.+/2, |
| 48 | + compare: &Yog.Utils.compare/2 |
| 49 | + ) |
| 50 | + |> case do |
| 51 | + {:ok, dist} -> dist |
| 52 | + :error -> :failed |
| 53 | + end |
| 54 | + end |
| 55 | + |
| 56 | + # Part 2: Four robots |
| 57 | + defp solve_part_2(lines) do |
| 58 | + grid = parse_grid(lines) |
| 59 | + grid = modify_for_part_2(grid) |
| 60 | + |
| 61 | + starts = ["1", "2", "3", "4"] |
| 62 | + pois = find_pois_part_2(grid, starts) |
| 63 | + all_keys_mask = calculate_keys_mask(pois) |
| 64 | + adj = build_poi_graph(grid, pois) |
| 65 | + |
| 66 | + initial_robots = starts |
| 67 | + |
| 68 | + Dijkstra.implicit_dijkstra( |
| 69 | + from: {initial_robots, 0}, |
| 70 | + successors_with_cost: fn {robots, collected} -> |
| 71 | + Enum.with_index(robots) |
| 72 | + |> Enum.flat_map(fn {at, idx} -> |
| 73 | + edges = Map.get(adj, at, []) |
| 74 | + |
| 75 | + edges |
| 76 | + |> Enum.filter(fn edge -> |
| 77 | + band(collected, edge.required) == edge.required and |
| 78 | + band(collected, key_bit(edge.to)) == 0 |
| 79 | + end) |
| 80 | + |> Enum.map(fn edge -> |
| 81 | + new_collected = bor(collected, key_bit(edge.to)) |
| 82 | + new_robots = List.replace_at(robots, idx, edge.to) |
| 83 | + {{new_robots, new_collected}, edge.dist} |
| 84 | + end) |
| 85 | + end) |
| 86 | + end, |
| 87 | + visited_by: fn {robots, collected} -> {robots, collected} end, |
| 88 | + is_goal: fn {_, collected} -> collected == all_keys_mask end, |
| 89 | + zero: 0, |
| 90 | + add: &Kernel.+/2, |
| 91 | + compare: &Yog.Utils.compare/2 |
| 92 | + ) |
| 93 | + |> case do |
| 94 | + {:ok, dist} -> dist |
| 95 | + :error -> :failed |
| 96 | + end |
| 97 | + end |
| 98 | + |
| 99 | + # --- Grid Utilities --- |
| 100 | + |
| 101 | + defp parse_grid(lines) do |
| 102 | + for {line, y} <- Enum.with_index(lines), |
| 103 | + {char, x} <- Enum.with_index(String.graphemes(line)), |
| 104 | + into: %{}, |
| 105 | + do: {{x, y}, char} |
| 106 | + end |
| 107 | + |
| 108 | + defp find_pois(grid, start_chars) do |
| 109 | + grid |
| 110 | + |> Enum.filter(fn {_, char} -> char in start_chars or is_key?(char) end) |
| 111 | + |> Map.new(fn {pos, char} -> {char, pos} end) |
| 112 | + end |
| 113 | + |
| 114 | + defp find_pois_part_2(grid, start_chars) do |
| 115 | + grid |
| 116 | + |> Enum.filter(fn {_, char} -> |
| 117 | + char in start_chars or is_key?(char) |
| 118 | + end) |
| 119 | + |> Map.new(fn {pos, char} -> {char, pos} end) |
| 120 | + end |
| 121 | + |
| 122 | + defp calculate_keys_mask(pois) do |
| 123 | + Enum.reduce(pois, 0, fn {label, _}, acc -> |
| 124 | + if is_key?(label), do: bor(acc, key_bit(label)), else: acc |
| 125 | + end) |
| 126 | + end |
| 127 | + |
| 128 | + defp build_poi_graph(grid, pois) do |
| 129 | + Map.new(pois, fn {label, pos} -> |
| 130 | + {label, find_reachable_from(grid, pos)} |
| 131 | + end) |
| 132 | + end |
| 133 | + |
| 134 | + defp find_reachable_from(grid, start_pos) do |
| 135 | + q = :queue.in({start_pos, 0, 0}, :queue.new()) |
| 136 | + visited = MapSet.new([start_pos]) |
| 137 | + bfs_poi(grid, q, visited, []) |
| 138 | + end |
| 139 | + |
| 140 | + # BFS that finds ALL reachable keys, tracking both doors AND keys as requirements |
| 141 | + defp bfs_poi(grid, q, visited, acc) do |
| 142 | + case :queue.out(q) do |
| 143 | + {:empty, _} -> |
| 144 | + acc |
| 145 | + |
| 146 | + {{:value, {pos, dist, mask}}, rest} -> |
| 147 | + char = grid[pos] |
| 148 | + |
| 149 | + # Record this key if we found one (but don't stop exploring!) |
| 150 | + new_acc = |
| 151 | + if dist > 0 and is_key?(char) do |
| 152 | + [%{to: char, dist: dist, required: mask} | acc] |
| 153 | + else |
| 154 | + acc |
| 155 | + end |
| 156 | + |
| 157 | + # Continue exploring - update mask with doors AND keys we pass through |
| 158 | + new_mask = |
| 159 | + cond do |
| 160 | + is_door?(char) -> bor(mask, door_bit(char)) |
| 161 | + is_key?(char) -> bor(mask, key_bit(char)) |
| 162 | + true -> mask |
| 163 | + end |
| 164 | + |
| 165 | + {nq, nv} = |
| 166 | + Enum.reduce(neighbors(pos), {rest, visited}, fn nb, {q_acc, v_acc} -> |
| 167 | + nb_char = Map.get(grid, nb) |
| 168 | + |
| 169 | + if nb_char != nil and nb_char != "#" and not MapSet.member?(v_acc, nb) do |
| 170 | + {:queue.in({nb, dist + 1, new_mask}, q_acc), MapSet.put(v_acc, nb)} |
| 171 | + else |
| 172 | + {q_acc, v_acc} |
| 173 | + end |
| 174 | + end) |
| 175 | + |
| 176 | + bfs_poi(grid, nq, nv, new_acc) |
| 177 | + end |
| 178 | + end |
| 179 | + |
| 180 | + defp neighbors({x, y}), do: [{x + 1, y}, {x - 1, y}, {x, y + 1}, {x, y - 1}] |
| 181 | + |
| 182 | + defp is_key?(char), do: char != nil and char >= "a" and char <= "z" |
| 183 | + defp is_door?(char), do: char != nil and char >= "A" and char <= "Z" |
| 184 | + |
| 185 | + defp key_bit(char), do: bsl(1, hd(String.to_charlist(char)) - ?a) |
| 186 | + defp door_bit(char), do: bsl(1, hd(String.to_charlist(char)) - ?A) |
| 187 | + |
| 188 | + defp modify_for_part_2(grid) do |
| 189 | + start_item = Enum.find(grid, fn {_, v} -> v == "@" end) |
| 190 | + |
| 191 | + if start_item do |
| 192 | + {cx, cy} = elem(start_item, 0) |
| 193 | + |
| 194 | + grid |
| 195 | + |> Map.merge(%{ |
| 196 | + {cx, cy} => "#", |
| 197 | + {cx + 1, cy} => "#", |
| 198 | + {cx - 1, cy} => "#", |
| 199 | + {cx, cy + 1} => "#", |
| 200 | + {cx, cy - 1} => "#", |
| 201 | + {cx - 1, cy - 1} => "1", |
| 202 | + {cx + 1, cy - 1} => "2", |
| 203 | + {cx - 1, cy + 1} => "3", |
| 204 | + {cx + 1, cy + 1} => "4" |
| 205 | + }) |
| 206 | + else |
| 207 | + grid |
| 208 | + end |
| 209 | + end |
| 210 | + |
| 211 | + def parse(data \\ input()), do: data |
| 212 | +end |
0 commit comments