Skip to content

Commit 3da4c13

Browse files
committed
Solve 2019/18
1 parent 7db283d commit 3da4c13

3 files changed

Lines changed: 305 additions & 0 deletions

File tree

lib/2019/day_18.ex

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
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

priv/input_files/2019_18.txt

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
#################################################################################
2+
#...#..j..........#.........#...........#.......#........n......#.....#........m#
3+
#.#.#.#.###########.###.#####.#######O#####.###.#.#########.#.###.#.###.###.###.#
4+
#.#.#.#...............#.Z.......#...#...#...#.#.#.#.......#.#.....#.....#.X.#...#
5+
#.#.#.###########################P#.#.#.#.###.#.###.#####.###.###########.###.###
6+
#.#.#.#.......#.........#......b..#.#.#.#.#.......#...#.#...#.....#.......#.....#
7+
#.#.#.#.###.###.#######.#.#########.###.#.#######.###.#.###.#####.#.#############
8+
#.#.#.#.#...#...#..l#...#.#.......#.#...#.....#.......#.#...#.....#.........#..k#
9+
#.###.#.###.#.#####.#.###.#######.#.#.###.###.#########.#.###.#####.#######.#.#.#
10+
#...#.#...#.#.#...#...#...#.....#.#...#.#...#...#.....#.#.#...#...#...#...#...#.#
11+
#.#.#.#.#.###.#.#U###.###.#.###.#.#####.###.###.#.###.#.#.#.###.#.#####.#.#####F#
12+
#.#.#.#.#...#...#...#.....#.#...#.......#...#g#...#.#.#...#.#...#...#.E.#.....#.#
13+
#.#.#.#.###.#######.#######.###.###Y#.###.###.#####.#.###.###.#####.#.#######.#.#
14+
#.#...#...#...#.....#......a..#...#.#...#.....#.....#...#.....#.......#.....#...#
15+
#.#####D#####.#.#######.#####.###.###.#.#####.#.###.###.#################.#####.#
16+
#.M...#.#...#.#.#c....#...#...#.#...#.#.#...#.#.#.#.#.#.#...............#...#...#
17+
#####.###.#.#.#H#.###.#####.###.###.#.#.#.###.#.#.#.#.#.#.###########.#.###.#.###
18+
#...#.....#.#.#.#...#...#...#.....#.#.#.#...#.#.#.....#.......#.....#.#.#...#.#.#
19+
#.#.#######.#.#.#.#####.#.###.#.###.###.#.#.#.#.#####.#########.###.###.#.###.#.#
20+
#.#.#....x#.#.#...#.....#.#...#.#..i#...#.#.#.#...#...#.........#.....#.#...#...#
21+
#.###K#.#.#.#.#####.#####.#.###.#.###.#####.#.###.#####.###.#########.#.#.#.###.#
22+
#...#.#.#.#.#.....#..r#...#...#.#.#.....#...#.....#.....#...#.......#...#.#.....#
23+
#.#.#.#.###.#.###.#####.#####.#.#.#.###.#.#####.###.#####.#########.#####.#####.#
24+
#.#.#.#.....#.#.........#.....#.#.#...#.#.......#...#...#.#.........#.....#...#.#
25+
###.#.#########.#########.#####.#.#####.#####.###.#.#.#.###.#########.#####.#.#.#
26+
#...#...#.....#.#...........#...#.....#.#..t#.#.#.#.#.#.....#...#.........#.#.#.#
27+
#.#####.#.#.#.#.#####.#####.#########.#.#.#.#.#.#.#.#.#######.###.#########.#.###
28+
#.......#.#.#.#.....#...#.#.....#...#.#.#.#.#...#.#.#.....#.......#...#...#.#...#
29+
#.#########.#.###.#.###.#.#####.#.#.#.#.#.#.#####.#.#####.#########.#.#.#.#.###.#
30+
#.#.....#...#...#.#.#...#...#...#.#...#.#.#.......#.#...#.......#q..#...#...#...#
31+
#.###.#.#.#####.###.###.#.###.###.#####.#.#########.###.#######.#.###########.###
32+
#.....#.#...#.#...#...#.#...#...#...#...#...#...#.#.....#.......#.#...#.....#...#
33+
#######.#.#.#.###.#.#.#.###.###.###.###.#.#.#.#.#.#####.#.#######.#.###.#.#####.#
34+
#.....#.#.#.#.....#.#.#.......#...#.....#.#.#.#.#.#.....#.#...#...#...#.#.....#.#
35+
#.#.###.#.#.###.#####.#####.###.#.#####.#.#.#.#.#.#.#####.#.#.#.#####.#.#####.#.#
36+
#.#.#...#.#...#.#...#.#.....#...#...#...#.#...#.#...#...#...#...#.....#.#.....#.#
37+
###.#.###.###.#.#.#.#.#######.#######.###.#####.#.###.#.#########.#.###.#.###.#.#
38+
#...#.#...#...#.#.#.#...#.....#.....#.#.#.#...#.#.#...#.......#...#.....#...#.#.#
39+
#.###.#####.###.#.#.###.#.#####.###.#.#.#.###.#.###.#######.###.###########.###.#
40+
#...........#.....#.....#.........#...........#...........#...............#w....#
41+
#######################################.@.#######################################
42+
#.....#.....#.......#.........#...................#.#...................#...#...#
43+
#.###.###.#.#.#####.#.#.#####.#.#####.#.#.#######.#.#.###############.#.#.#.#.#.#
44+
#...#.....#...#.#...#.#...#.#.#.#.#...#.#.......#...#...#...#...#...#.#...#...#.#
45+
###.#####.#####.#.#######.#.#.#.#.#.###.#.#####.###.###.#.###.#.#.#.#.#########.#
46+
#...#...#.#...#.#.....#...#...#...#...#.#.#...#...#.#...#.#...#...#.#.........#.#
47+
#.###.#.###.#.#.#####.#.###.#########.#.#.###.###.###.###.#.#######.###########.#
48+
#.....#.....#.#.....#...#.#.#.......#.#.#.......#.....#...#.#...................#
49+
#############.#####.#####.#.#.#####.#.#.#######.#######.#.#.###.###############.#
50+
#...........#.....#.......#.#...#.#.#.#.#.....#.........#.#...#.#.........#...#.#
51+
#########.#.#####.#.###.###.###.#.#.#.###.#.#.#######T#######.###.#######.#.###.#
52+
#........f#.#...#.#...#.....#...#...#...#.#.#.#.....#.#.......#...#...#...#.#...#
53+
#.#######.###.#.#.#####.#####.###.#####.#.#.#.#.###.#.#.#######.#####.#.###.#.###
54+
#.#.#.....#...#.#.....#.......#...#.....#.#.#.#.#.#...#.#...#.......#.#.#.......#
55+
#.#.#.#####.###.#.###.#########.###.#.###.#.###.#.#####.#.#.#.#####.#.#.#######.#
56+
#.#.#...#...#.#.#...#.#...#...#.#.#.#...#.#...#.#...#...#.#...#...#...#.#.....#.#
57+
#.#.###.#.###.#.###.#.###.#.#.#.#.#.###.#####.#.#.#.#.###.#####.#.#####.#.###.#.#
58+
#.#.....#.#...#...#.#...#...#.#.#...#.#.#.....#.#.#.#.......#...#.#...#....s#.#v#
59+
#.#.#####.#.#.###.###.#.#.###.#.###.#.#.#.#####.###.#####.###.###.#.#.#######.#.#
60+
#.#...#...#.#...#...#.#.#...#.#...#..y#.#.#.........#...#.#...#.....#.........#.#
61+
#.###.#.#######.###.###.#####.###.#####.#.#.#####.###.#.###.#########.###########
62+
#.#...#.........#.#...#.....#...#.#.....#...#...#.#...#...#.#.......#.#.........#
63+
#.#.###########.#.#########.#.###.#.#########.#.#.#.#####.#.#######R###.#######.#
64+
#.#.#.....Q...#...#.........#...#.#.#...#...#.#.#.#.#...#.....#...#.....#...#.C.#
65+
#.###.#.#########.#.###.#######.#.#.#.#.#.#.#.#.#.#.###.#####.#.#.#########.#.###
66+
#...#.#...#...G.#.#...#.#.#.....#.#...#.#.#.#.#.#.#...#...#...#.#.....#.....#.#.#
67+
#.#.#.###.#.###.#.#####.#.#.#.###.#####.#.#.#.#.#.###.#.#.#.#####.###.#.#####.#.#
68+
#.#.#...#.#.#...#.#.....#.#.#.#...#...I.#.#...#.#...#.#.#.#.#.....#.#...#...#...#
69+
#.#S###.#.#.#.#.#.#.#####.#.#.#.###.###.#.#####.#####.#.#.#.###.###.###.#.#.###.#
70+
#.#.....#...#.#.#...#.......#...#...#.#.#.....#.......#.#.#...#.#.#...#.#.#.....#
71+
#.###########.#######.###########.###.#.#.###.#########.#.###.#.#.#.#.#.#.#######
72+
#e#.......#...#.....#.#.......#...#.#...#.#...#...#.#...#.......#...#...#.#.....#
73+
#.#.#######.#.#.#.###.#.###.###.#.#.#.###N#.###.#.#.#.#########.#####.###.#.###.#
74+
#.#.#.......#.#.#.....#...#.#...#.#.....#.#.#...#.#.#.#..p..#...#...#...#h..#.#.#
75+
#.#.#.#########W#########.#.#.###.#####.###.#.###.#.#.#.###.#####.#B#########.#.#
76+
#...#d....#...#.....#...#.#.....#.....#.#...#.#.#.#.#.#...#.#.....#.#.......#...#
77+
###.#####.#.#.#####.#.#.###.#######.#.#.#.###.#.#.#.#.###.#.#.#####.#.#####.#.###
78+
#...#...#...#.....#...#.V.#.#.....#.#.#.#.#...#z#.#...#...#.#...#.#...#.#.L.#.#.#
79+
#.#####.#######.#########.###.###.###.#.#.#.###J#.#####.###.###.#.#####.#.###.#.#
80+
#.............#...............#.....A.#.#.......#........o#.............#....u..#
81+
#################################################################################

test/2019/day_18_test.exs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
defmodule AdventOfCode.Y2019.Day18Test do
2+
@moduledoc false
3+
4+
use ExUnit.Case, async: true
5+
@moduletag :y1918
6+
7+
alias AdventOfCode.Y2019.Day18, as: Solution
8+
9+
test "Year 2019, Day 18 run/1" do
10+
assert Solution.run() == {6098, 1698}
11+
end
12+
end

0 commit comments

Comments
 (0)