Skip to content

Commit

Permalink
donee!
Browse files Browse the repository at this point in the history
  • Loading branch information
Tom-the-Bomb committed Dec 30, 2023
1 parent eda7d95 commit 8cbc70b
Show file tree
Hide file tree
Showing 11 changed files with 795 additions and 32 deletions.
6 changes: 5 additions & 1 deletion aoc-py/solutions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
'Day20',
'Day21',
'Day22',
'Day23',
'Day24',
'Day25',
)

Expand All @@ -47,6 +49,8 @@
from .day20 import Day20
from .day21 import Day21
from .day22 import Day22
from .day23 import Day23
from .day24 import Day24
from .day25 import Day25

from ..solution import Solution
Expand All @@ -56,7 +60,7 @@
Day6, Day7, Day8, Day9, Day10,
Day11, Day12, Day13, Day14, Day15,
Day16, Day17, Day18, Day19, Day20,
Day21, Day22, Day25,
Day21, Day22, Day23, Day24, Day25,
)

del Solution
12 changes: 7 additions & 5 deletions aoc-py/solutions/day22.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,23 +173,25 @@ def part_two(self, inp: str) -> int:
# we go through all bricks `b` supports and include it (`a`)
# if `a` is supported by only 1 brick, which has to be `b`
to_check = [
a for a in supports[b] if len(supported_by[a]) == 1
a for a in supports[b]
if len(supported_by[a]) == 1
]
# bricks that are going to fall
falling = set(to_check + [b])
falling = set(to_check)

while to_check:
to_fall = to_check.pop(0)

# go through all bricks that are supported by `to_fall`
# that is also NOT already falling
for a in supports[to_fall].difference(falling):
# if everything that supports `a` is ALL falling
# if everything that supports `a` are ALL falling
# meaning `a` itself is no longer supported
if supported_by[a].issubset(falling):
to_check.append(a)
falling.add(a)
# we need to substract `1` as we included `b` the original disintegrated brick
total += len(falling) - 1
# add # of falling bricks for each brick `b`
total += len(falling)
return total

def run(self, inp: str) -> None:
Expand Down
127 changes: 127 additions & 0 deletions aoc-py/solutions/day23.py
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
189 changes: 189 additions & 0 deletions aoc-py/solutions/day24.py
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
Loading

0 comments on commit 8cbc70b

Please sign in to comment.