Skip to content

Commit 0cb3c58

Browse files
committed
AoC 2024 Day 16 - cleanup
1 parent 5c0354f commit 0cb3c58

File tree

1 file changed

+100
-108
lines changed

1 file changed

+100
-108
lines changed

src/main/python/AoC2024_16.py

Lines changed: 100 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,32 @@
33
# Advent of Code 2024 Day 16
44
#
55

6+
from __future__ import annotations
7+
8+
import itertools
69
import sys
710
from collections import defaultdict
811
from queue import PriorityQueue
912
from typing import Callable
1013
from typing import Iterator
11-
from typing import TypeVar
14+
from typing import NamedTuple
15+
from typing import Self
1216

1317
from aoc.common import InputData
1418
from aoc.common import SolutionBase
1519
from aoc.common import aoc_samples
1620
from aoc.geometry import Direction
1721
from aoc.geometry import Turn
18-
19-
# from aoc.graph import a_star
2022
from aoc.grid import Cell
2123
from aoc.grid import CharGrid
2224

23-
T = TypeVar("T")
24-
2525
Input = CharGrid
2626
Output1 = int
2727
Output2 = int
28+
State = tuple[Cell, str]
29+
30+
DIRS = {"U", "R", "D", "L"}
31+
START_DIR = "R"
2832

2933

3034
TEST1 = """\
@@ -65,120 +69,108 @@
6569
"""
6670

6771

68-
def dijkstra(
69-
starts: set[T],
70-
is_end: Callable[[T], bool],
71-
adjacent: Callable[[T], Iterator[T]],
72-
get_cost: Callable[[T, T], int],
73-
) -> dict[T, int]:
74-
q: PriorityQueue[tuple[int, T]] = PriorityQueue()
75-
for s in starts:
76-
q.put((0, s))
77-
best: defaultdict[T, int] = defaultdict(lambda: sys.maxsize)
78-
for s in starts:
79-
best[s] = 0
80-
while not q.empty():
81-
cost, node = q.get()
82-
c_total = best[node]
83-
for n in adjacent(node):
84-
new_risk = c_total + get_cost(node, n)
85-
if new_risk < best[n]:
86-
best[n] = new_risk
87-
q.put((new_risk, n))
88-
return best
89-
90-
9172
class Solution(SolutionBase[Input, Output1, Output2]):
92-
def parse_input(self, input_data: InputData) -> Input:
93-
return CharGrid.from_strings(list(input_data))
94-
95-
def part_1(self, grid: Input) -> Output1:
96-
def adjacent(state: tuple[Cell, str]) -> Iterator[tuple[Cell, str]]:
97-
cell, letter = state
98-
dir = Direction.from_str(letter)
73+
class ReindeerMaze(NamedTuple):
74+
grid: CharGrid
75+
start: Cell
76+
end: Cell
77+
78+
@classmethod
79+
def from_grid(cls, grid: CharGrid) -> Self:
80+
for cell in grid.get_cells():
81+
val = grid.get_value(cell)
82+
if val == "S":
83+
start = cell
84+
if val == "E":
85+
end = cell
86+
return cls(grid, start, end)
87+
88+
def get_turns(self, direction: Direction) -> Iterator[str]:
9989
for turn in (Turn.LEFT, Turn.RIGHT):
100-
new_letter = dir.turn(turn).letter
90+
new_letter = direction.turn(turn).letter
10191
assert new_letter is not None
102-
yield (cell, new_letter)
103-
nxt = cell.at(dir)
104-
if grid.get_value(nxt) != "#":
105-
yield (cell.at(dir), letter)
106-
107-
def cost(curr: tuple[Cell, str], next: tuple[Cell, str]) -> int:
108-
if curr[1] == next[1]:
109-
return 1
110-
else:
111-
return 1000
112-
113-
start = next(grid.get_all_equal_to("S"))
114-
end = next(grid.get_all_equal_to("E"))
115-
distance = dijkstra(
116-
{(start, "R")},
117-
lambda node: node[0] == end,
118-
adjacent,
119-
cost,
120-
)
121-
key = next(k for k in distance.keys() if k[0] == end)
122-
return distance[key]
92+
yield new_letter
12393

124-
def part_2(self, grid: Input) -> Output2:
125-
def adjacent_1(state: tuple[Cell, str]) -> Iterator[tuple[Cell, str]]:
94+
def adjacent_forward(self, state: State) -> Iterator[State]:
12695
cell, letter = state
127-
dir = Direction.from_str(letter)
128-
for turn in (Turn.LEFT, Turn.RIGHT):
129-
new_letter = dir.turn(turn).letter
130-
assert new_letter is not None
131-
yield (cell, new_letter)
132-
nxt = cell.at(dir)
133-
if grid.get_value(nxt) != "#":
96+
direction = Direction.from_str(letter)
97+
for d in self.get_turns(direction):
98+
yield (cell, d)
99+
nxt = cell.at(direction)
100+
if self.grid.get_value(nxt) != "#":
134101
yield (nxt, letter)
135102

136-
def adjacent_2(state: tuple[Cell, str]) -> Iterator[tuple[Cell, str]]:
103+
def adjacent_backward(self, state: State) -> Iterator[State]:
137104
cell, letter = state
138-
dir = Direction.from_str(letter)
139-
for turn in (Turn.LEFT, Turn.RIGHT):
140-
new_letter = dir.turn(turn).letter
141-
assert new_letter is not None
142-
yield (cell, new_letter)
143-
nxt = cell.at(dir.turn(Turn.AROUND))
144-
if grid.get_value(nxt) != "#":
105+
direction = Direction.from_str(letter)
106+
for d in self.get_turns(direction):
107+
yield (cell, d)
108+
nxt = cell.at(direction.turn(Turn.AROUND))
109+
if self.grid.get_value(nxt) != "#":
145110
yield (nxt, letter)
146111

147-
def cost(curr: tuple[Cell, str], next: tuple[Cell, str]) -> int:
148-
if curr[1] == next[1]:
149-
return 1
150-
else:
151-
return 1000
152-
153-
start = next(grid.get_all_equal_to("S"))
154-
end = next(grid.get_all_equal_to("E"))
155-
distance_1 = dijkstra(
156-
{(start, "R")},
157-
lambda node: node[0] == end,
158-
adjacent_1,
159-
cost,
160-
)
161-
key = next(k for k in distance_1.keys() if k[0] == end)
162-
best = distance_1[key]
163-
distance_2 = dijkstra(
164-
{(end, "U"), (end, "R"), (end, "D"), (end, "L")},
165-
lambda node: node[0] == start,
166-
adjacent_2,
167-
cost,
112+
def dijkstra(
113+
self,
114+
starts: set[State],
115+
is_end: Callable[[State], bool],
116+
adjacent: Callable[[State], Iterator[State]],
117+
get_distance: Callable[[State, State], int],
118+
) -> dict[State, int]:
119+
q: PriorityQueue[tuple[int, State]] = PriorityQueue()
120+
for s in starts:
121+
q.put((0, s))
122+
dists: defaultdict[State, int] = defaultdict(lambda: sys.maxsize)
123+
for s in starts:
124+
dists[s] = 0
125+
while not q.empty():
126+
dist, node = q.get()
127+
curr_dist = dists[node]
128+
for n in adjacent(node):
129+
new_dist = curr_dist + get_distance(node, n)
130+
if new_dist < dists[n]:
131+
dists[n] = new_dist
132+
q.put((new_dist, n))
133+
return dists
134+
135+
def forward_distances(self) -> dict[State, int]:
136+
return self.dijkstra(
137+
{(self.start, START_DIR)},
138+
lambda node: node[0] == self.end,
139+
self.adjacent_forward,
140+
lambda curr, nxt: 1 if curr[1] == nxt[1] else 1000,
141+
)
142+
143+
def backward_distances(self) -> dict[State, int]:
144+
return self.dijkstra(
145+
{_ for _ in itertools.product([self.end], DIRS)},
146+
lambda node: node[0] == self.start,
147+
self.adjacent_backward,
148+
lambda curr, nxt: 1 if curr[1] == nxt[1] else 1000,
149+
)
150+
151+
def parse_input(self, input_data: InputData) -> Input:
152+
return CharGrid.from_strings(list(input_data))
153+
154+
def part_1(self, grid: Input) -> Output1:
155+
maze = Solution.ReindeerMaze.from_grid(grid)
156+
distances = maze.forward_distances()
157+
return next(v for k, v in distances.items() if k[0] == maze.end)
158+
159+
def part_2(self, grid: Input) -> Output2:
160+
maze = Solution.ReindeerMaze.from_grid(grid)
161+
forw_dists = maze.forward_distances()
162+
best = next(v for k, v in forw_dists.items() if k[0] == maze.end)
163+
backw_dists = maze.backward_distances()
164+
all_tile_states = itertools.product(
165+
grid.find_all_matching(lambda cell: grid.get_value(cell) != "#"),
166+
DIRS,
168167
)
169-
ans = set[Cell]([end])
170-
for cell in grid.find_all_matching(
171-
lambda cell: grid.get_value(cell) != "#"
172-
):
173-
for dir in ("U", "R", "D", "L"):
174-
if (
175-
(cell, dir) in distance_1
176-
and (cell, dir) in distance_2
177-
and distance_1[(cell, dir)] + distance_2[(cell, dir)]
178-
== best
179-
):
180-
ans.add(cell)
181-
return len(ans)
168+
best_tiles = {
169+
cell
170+
for cell, dir in all_tile_states
171+
if forw_dists[(cell, dir)] + backw_dists[(cell, dir)] == best
172+
}
173+
return len(best_tiles)
182174

183175
@aoc_samples(
184176
(

0 commit comments

Comments
 (0)