-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
eda7d95
commit 8cbc70b
Showing
11 changed files
with
795 additions
and
32 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
""" | ||
Day 23: A Long Walk | ||
https://adventofcode.com/2023/day/23 | ||
""" | ||
__all__ = ('Day23',) | ||
|
||
from typing import ClassVar | ||
from collections import defaultdict | ||
|
||
from ..solution import Solution | ||
|
||
class Day23(Solution): | ||
NAME: ClassVar[str] = 'A Long Walk' | ||
|
||
def _get_neighbors(self, grid: list[str], i: int, j: int) -> dict[str, tuple[int, int]]: | ||
"""Returns the 4-neighborhood tiles of (i, j) | ||
while ensuring it is not a maze wall ('#') | ||
and that it does not go out of the grid bounds | ||
""" | ||
n_rows = len(grid) | ||
n_cols = len(grid[0]) | ||
|
||
return { | ||
direction: (i, j) | ||
for direction, (i, j) in ( | ||
('>', (i, j + 1)), | ||
('<', (i, j - 1)), | ||
('v', (i + 1, j)), | ||
('^', (i - 1, j)), | ||
) | ||
if i in range(n_rows) | ||
and j in range(n_cols) | ||
and grid[i][j] != '#' | ||
} | ||
|
||
def _hike(self, inp: str, *, slopes: bool) -> int: | ||
grid = inp.splitlines() | ||
|
||
start = (0, grid[0].index('.')) | ||
|
||
last_row = len(grid) - 1 | ||
end = (last_row, grid[last_row].index('.')) | ||
|
||
nodes = [start, end] | ||
|
||
for i, row in enumerate(grid): | ||
for j, tile in enumerate(row): | ||
if tile != '#' and len(self._get_neighbors(grid, i, j)) >= 3: | ||
# crossroad points, where we can make a choice of where to go | ||
# they will be the nodes for the constructed graph | ||
nodes.append((i, j)) | ||
|
||
# graph of nodes (crossroads) + edges between them | ||
# | ||
# maps: node -> (map: connected nodes -> distance away from starting node) | ||
# only stores directly adjacent nodes | ||
graph = defaultdict(dict) | ||
|
||
for starting_node in nodes: | ||
to_check = [(starting_node, int())] | ||
seen = {starting_node} | ||
|
||
while to_check: | ||
# distance = edge length | ||
node, distance = to_check.pop() | ||
|
||
if distance > 0 and node in nodes: | ||
# we've reached a crossroad/node, we can add it to the graph | ||
graph[starting_node][node] = distance | ||
else: | ||
# hit a regular tile | ||
# keep on traversing, "floodfilling" through the neighbors of each tile | ||
row, col = node | ||
connected_nodes = self._get_neighbors(grid, row, col) | ||
|
||
for row, col in ( | ||
[slope] | ||
# we've reached a slope tile: '<', '>', '^', 'v' | ||
# obtain the single direction (neighbor in that direction) | ||
# that we are allowed to go to according to the slope | ||
if slopes and (slope := connected_nodes.get(grid[row][col])) | ||
# '.' tile, regular tile, check all 4 neighbors | ||
else connected_nodes.values() | ||
): | ||
if (node := (row, col)) not in seen: | ||
# add tile as part of edge, increment distance | ||
to_check.append((node, distance + 1)) | ||
seen.add(node) | ||
|
||
seen = set() | ||
|
||
def _dfs(node: tuple[int, int]) -> int: | ||
"""Brutes force the length of the longest path | ||
depth-first-search traversal through all path possibilities using generated graph | ||
""" | ||
if node == end: | ||
return 0 | ||
# `seen` set to avoid cycles. | ||
seen.add(node) | ||
|
||
max_length = max([ | ||
# recursively pathfinds, and adds up all the distances of the edges | ||
_dfs(next_node) + graph[node][next_node] | ||
for next_node in graph[node] | ||
if next_node not in seen | ||
] + [0]) # `0` if no path found, avoids max() arg is an empty sequence error | ||
|
||
seen.remove(node) | ||
return max_length | ||
|
||
return _dfs(start) | ||
|
||
def part_one(self, inp: str) -> int: | ||
return self._hike(inp, slopes=True) | ||
|
||
def part_two(self, inp: str) -> int: | ||
return self._hike(inp, slopes=False) | ||
|
||
def run(self, inp: str) -> None: | ||
print('Part 1:', p1 := self.part_one(inp)) | ||
print('Part 2:', p2 := self.part_two(inp)) | ||
|
||
assert p1 == 2182 | ||
assert p2 == 6670 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,189 @@ | ||
from __future__ import annotations | ||
""" | ||
Day 24: Never Tell Me The Odds | ||
https://adventofcode.com/2024/day/24 | ||
""" | ||
__all__ = ('Day24',) | ||
|
||
from typing import ClassVar, Optional | ||
|
||
import z3 | ||
import numpy as np | ||
|
||
from ..solution import Solution | ||
|
||
class Hailstone: | ||
"""Represents a hailstone, storing it's velocity and starting-position values""" | ||
def __init__( | ||
self, | ||
x_pos: int, | ||
y_pos: int, | ||
z_pos: int, | ||
x_vel: int, | ||
y_vel: int, | ||
z_vel: int, | ||
) -> None: | ||
self.x_pos = x_pos | ||
self.y_pos = y_pos | ||
self.z_pos = z_pos | ||
|
||
self.x_vel = x_vel | ||
self.y_vel = y_vel | ||
self.z_vel = z_vel | ||
|
||
# convert to y = mx + b | ||
self.m = self.y_vel / self.x_vel | ||
# given y = mx + b | ||
# y - mx = b | ||
self.b = self.y_pos - self.m * self.x_pos | ||
|
||
@classmethod | ||
def from_str(cls, raw: str) -> Hailstone: | ||
pos, vel = raw.split('@') | ||
return cls( | ||
*map(int, pos.split(',')), | ||
*map(int, vel.split(',')), | ||
) | ||
|
||
def evaluate(self, x: float) -> float: | ||
"""Evaluates f(x) = mx + b""" | ||
return self.m * x + self.b | ||
|
||
def in_domain(self, x: float, y: float) -> bool: | ||
"""Ensures the hailstones do not intersect in the past (only when t >= 0) | ||
We represent the hailstones as a line of `mx + b` | ||
but in reality they are rays, starting at (x_pos, y_pox) | ||
This method checks whether or not a coordinate (x, y) is on said ray | ||
""" | ||
return ( | ||
# x-values check based on the sign of `x_vel`, which indicates which side of the `y_pos` we should be on | ||
x >= self.x_pos if self.x_vel > 0 | ||
else x == self.x_pos if self.x_vel == 0 | ||
else x <= self.x_pos | ||
) and ( | ||
# y-values check based on the sign of `y_vel`, which indicates which side of the `y_pos` we should be on | ||
y >= self.y_pos if self.y_vel > 0 | ||
else y == self.y_pos if self.y_vel == 0 | ||
else y <= self.y_pos | ||
) | ||
|
||
def intersection(self, other: Hailstone) -> Optional[tuple[float, float]]: | ||
try: | ||
# given 2 lines: | ||
# f(x) = mx + b | ||
# g(x) = nx + c | ||
# intersection: f(x) = g(x): | ||
# mx + b = nx + c | ||
# mx - nx = c - b | ||
# x(m - n) = c - b | ||
# x = (c - b) / (m - n) | ||
x = (other.b - self.b) / (self.m - other.m) | ||
return (x, self.evaluate(x)) | ||
except ZeroDivisionError: | ||
# slopes are equal -> parallel lines -> no intersection | ||
return | ||
|
||
def __repr__(self) -> str: | ||
return f'<{self.__class__.__name__} [y = {self.m}x + {self.b}]>' | ||
|
||
class Day24(Solution): | ||
NAME: ClassVar[str] = 'Never Tell Me The Odds' | ||
|
||
def part_one(self, inp: str) -> int: | ||
hailstones = [ | ||
Hailstone.from_str(line) for line in inp.splitlines() | ||
] | ||
total = 0 | ||
for i, hs1 in enumerate(hailstones): | ||
for hs2 in hailstones[:i]: | ||
if point := hs1.intersection(hs2): | ||
x, y = point | ||
if ( | ||
hs1.in_domain(x, y) | ||
and hs2.in_domain(x, y) | ||
and 200000000000000 <= x <= 400000000000000 | ||
and 200000000000000 <= y <= 400000000000000 | ||
): | ||
total += 1 | ||
return total | ||
|
||
def part_two_linalg(self, inp: str) -> int: | ||
"""Part 2 solved using linear algebra""" | ||
hs1, hs2, hs3, *_ = [Hailstone.from_str(line) for line in inp.splitlines()] | ||
|
||
# solving for `x` in Ax + b where (A = a) | ||
a = np.array( | ||
[ | ||
[hs2.y_vel - hs1.y_vel, hs1.x_vel - hs2.x_vel, 0, hs1.y_pos - hs2.y_pos, hs2.x_pos - hs1.x_pos, 0], | ||
[hs3.y_vel - hs1.y_vel, hs1.x_vel - hs3.x_vel, 0, hs1.y_pos - hs3.y_pos, hs3.x_pos - hs1.x_pos, 0], | ||
[hs2.z_vel - hs1.z_vel, 0, hs1.x_vel - hs2.x_vel, hs1.z_pos - hs2.z_pos, 0, hs2.x_pos - hs1.x_pos], | ||
[hs3.z_vel - hs1.z_vel, 0, hs1.x_vel - hs3.x_vel, hs1.z_pos - hs3.z_pos, 0, hs3.x_pos - hs1.x_pos], | ||
[0, hs2.z_vel - hs1.z_vel, hs1.y_vel - hs2.y_vel, 0, hs1.z_pos - hs2.z_pos, hs2.y_pos - hs1.y_pos], | ||
[0, hs3.z_vel - hs1.z_vel, hs1.y_vel - hs3.y_vel, 0, hs1.z_pos - hs3.z_pos, hs3.y_pos - hs1.y_pos], | ||
] | ||
) | ||
|
||
b = np.array([ | ||
hs1.y_pos * hs1.x_vel - hs2.y_pos * hs2.x_vel - (hs1.x_pos * hs1.y_vel - hs2.x_pos * hs2.y_vel), | ||
hs1.y_pos * hs1.x_vel - hs3.y_pos * hs3.x_vel - (hs1.x_pos * hs1.y_vel - hs3.x_pos * hs3.y_vel), | ||
hs1.z_pos * hs1.x_vel - hs2.z_pos * hs2.x_vel - (hs1.x_pos * hs1.z_vel - hs2.x_pos * hs2.z_vel), | ||
hs1.z_pos * hs1.x_vel - hs3.z_pos * hs3.x_vel - (hs1.x_pos * hs1.z_vel - hs3.x_pos * hs3.z_vel), | ||
hs1.z_pos * hs1.y_vel - hs2.z_pos * hs2.y_vel - (hs1.y_pos * hs1.z_vel - hs2.y_pos * hs2.z_vel), | ||
hs1.z_pos * hs1.y_vel - hs3.z_pos * hs3.y_vel - (hs1.y_pos * hs1.z_vel - hs3.y_pos * hs3.z_vel), | ||
]) | ||
|
||
return round(sum(np.linalg.solve(a, b)[:3])) # type: ignore (`solve` function is not known) | ||
|
||
def part_two(self, inp: str) -> int: | ||
"""Part 2 solved using z3-solver""" | ||
|
||
# rock position and velocity variables | ||
x_pos, y_pos, z_pos, x_vel, y_vel, z_vel = z3.Reals( | ||
'x_pos, y_pos, z_pos, x_vel, y_vel, z_vel' | ||
) | ||
solver = z3.Solver() | ||
|
||
for i, line in enumerate(inp.splitlines()): | ||
hailstone = Hailstone.from_str(line) | ||
|
||
time = z3.Real(f't_{i}') | ||
|
||
solver.add(time >= 0) | ||
solver.add( | ||
# rock `x` position at `time` | ||
x_pos + time * x_vel | ||
# hailstone `y` position at `time` | ||
== hailstone.x_pos + time * hailstone.x_vel # type: ignore (addition of a `z3` variable to an `int` is not known) | ||
) | ||
solver.add( | ||
# rock `y` position at `time` | ||
y_pos + time * y_vel | ||
# hailstone `y` position at `time` | ||
== hailstone.y_pos + time * hailstone.y_vel # type: ignore | ||
) | ||
solver.add( | ||
# rock `z` position at `time` | ||
z_pos + time * z_vel | ||
# hailstone `z` position at `time` | ||
== hailstone.z_pos + time * hailstone.z_vel # type: ignore | ||
) | ||
assert solver.check() == z3.sat | ||
model = solver.model() | ||
|
||
return ( | ||
model[x_pos].as_long() # type: ignore (`as_long` method is not known) | ||
+ model[y_pos].as_long() # type: ignore | ||
+ model[z_pos].as_long() # type: ignore | ||
) | ||
|
||
def run(self, inp: str) -> None: | ||
print('Part 1:', p1 := self.part_one(inp)) | ||
print('Part 2:', p2 := self.part_two_linalg(inp)) | ||
|
||
assert p2 == self.part_two_linalg(inp) | ||
|
||
assert p1 == 14672 | ||
assert p2 == 646810057104753 |
Oops, something went wrong.