-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathday17.py
More file actions
215 lines (166 loc) · 5.67 KB
/
day17.py
File metadata and controls
215 lines (166 loc) · 5.67 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
from dataclasses import dataclass, field
from heapq import heappop, heappush
from pathlib import Path
from typing import Iterator, Optional
from loguru import logger
from typer import Typer
from utils import timer
main = Typer()
TURNS: dict[tuple[int, int], list[tuple[int, int]]] = {
(0, 1): [(1, 0), (-1, 0)],
(0, -1): [(1, 0), (-1, 0)],
(1, 0): [(0, 1), (0, -1)],
(-1, 0): [(0, 1), (0, -1)],
}
@dataclass
class Crucible:
row: int = 0
col: int = 0
delta_row: int = 0
delta_col: int = 0
heat_loss: int = 0
straight_counter: int = 0
previous: Optional["Crucible"] = field(default=None, repr=False)
def __lt__(self, other: "Crucible") -> bool:
return self.heat_loss < other.heat_loss
@property
def coord(self) -> tuple[int, int]:
return (self.row, self.col)
@property
def delta_coord(self) -> tuple[int, int]:
return (self.delta_row, self.delta_col)
@property
def state(self) -> tuple[int, int, int, int, int]:
return self.row, self.col, self.delta_row, self.delta_col, self.straight_counter
def neighbors(
self, grid: "Grid", min_straight: int = 0, max_straight: int = 3
) -> Iterator["Crucible"]:
"""Get admissible neighboring crucibles
Args:
grid: Grid with heat loss values
min_straight: Minimum number of straight steps before a turn
can be taken.
max_straight: Maximum number of straight steps before a turn
must be taken.
Yields:
Admissible neighboring crucibles
"""
deltas_straight: list[tuple[int, int, int]] = []
if self.straight_counter < max_straight:
deltas_straight.append((*self.delta_coord, self.straight_counter))
if self.straight_counter >= min_straight:
deltas_straight.extend((*delta, 0) for delta in TURNS[self.delta_coord])
for delta_row, delta_col, straight_counter in deltas_straight:
row = self.row + delta_row
col = self.col + delta_col
if 0 <= row < grid.height and 0 <= col < grid.width:
yield Crucible(
row=row,
col=col,
delta_row=delta_row,
delta_col=delta_col,
heat_loss=self.heat_loss + grid[row, col],
straight_counter=straight_counter + 1,
previous=self,
)
@dataclass
class Grid:
grid: list[list[int]]
@classmethod
def from_input(cls, input: str) -> "Grid":
return cls(grid=[[int(c) for c in line] for line in input.split("\n")])
@property
def height(self) -> int:
return len(self.grid)
@property
def width(self) -> int:
return len(self.grid[0])
def __getitem__(self, coord: tuple[int, int]) -> int:
row, col = coord
return self.grid[row][col]
def minimum_heat_loss(self, ultra: bool = False) -> int:
"""Compute path with minimum heat loss
Implemented via dijkstra's algorithm
Args:
ultra: If true, use ultra crucibles, else normal crucibles. Defaults to False.
Returns:
Total heat loss
"""
heap = [Crucible(delta_col=0, delta_row=1), Crucible(delta_col=1, delta_row=0)]
visited: set[tuple[int, int, int, int, int]] = set()
if ultra:
min_straight = 4
max_straight = 10
else:
min_straight = 0
max_straight = 3
while heap:
crucible = heappop(heap)
if crucible.state in visited:
continue
visited.add(crucible.state)
if crucible.coord == (self.height - 1, self.width - 1) and (
crucible.straight_counter >= min_straight
):
return crucible.heat_loss
for neighbor in crucible.neighbors(
self, min_straight=min_straight, max_straight=max_straight
):
heappush(heap, neighbor)
def visualize_path(self, crucible: Crucible) -> str:
"""Visualize the path
For debugging purposes
Args:
crucible: End of the path
Returns:
String representation of grid with inscribed path
"""
path: set[tuple[int, int]] = set()
heat_loss = 0
counter = 0
while crucible is not None:
path.add(crucible.coord)
heat_loss += self[*crucible.coord]
counter += 1
crucible = crucible.previous
return (
"\n".join(
"".join(
"." if (row, col) in path else str(self[row, col])
for col in range(self.width)
)
for row in range(self.height)
)
+ "\nHeat loss: "
+ str(heat_loss)
+ "\nCounter: "
+ str(counter)
)
@timer
def task01(input: str) -> int:
"""Solution for task 01
Args:
input: Grid string representation
Returns:
Total heat loss for normal crucibles
"""
grid = Grid.from_input(input)
return grid.minimum_heat_loss()
@timer
def task02(input: str) -> int:
"""Solution for task 02
Args:
input: Grid string representation
Returns:
Total heat loss for ultra crucibles
"""
grid = Grid.from_input(input)
return grid.minimum_heat_loss(ultra=True)
@main.command()
def entrypoint(path: Path):
with open(path, "r") as f:
input = f.read().strip()
logger.info(f"Task01: {task01(input)}")
logger.info(f"Task02: {task02(input)}")
if __name__ == "__main__":
main()