Skip to content

Commit

Permalink
day 18 + day 17 fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
Tom-the-Bomb committed Dec 19, 2023
1 parent 2df6d2c commit 77cdd44
Show file tree
Hide file tree
Showing 7 changed files with 915 additions and 11 deletions.
4 changes: 3 additions & 1 deletion aoc-py/solutions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
'Day15',
'Day16',
'Day17',
'Day18',
)

from .day1 import Day1
Expand All @@ -36,14 +37,15 @@
from .day15 import Day15
from .day16 import Day16
from .day17 import Day17
from .day18 import Day18

from ..solution import Solution

SOLUTIONS: tuple[type[Solution], ...] = (
Day1, Day2, Day3, Day4, Day5,
Day6, Day7, Day8, Day9, Day10,
Day11, Day12, Day13, Day14, Day15,
Day16, Day17,
Day16, Day17, Day18,
)

del Solution
1 change: 0 additions & 1 deletion aoc-py/solutions/day16.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from __future__ import annotations
"""
Day 16: The Floor Will Be Lava
Expand Down
13 changes: 6 additions & 7 deletions aoc-py/solutions/day17.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from __future__ import annotations
"""
Day 17: Clumsy Crucible
Expand All @@ -20,7 +19,7 @@ class Day17(Solution):
(1, 0), # right
)

def find_path(self, inp: str, *, is_part_two: bool) -> int:
def _find_path(self, inp: str, *, is_part_two: bool) -> int:
grid = [
[int(block) for block in row]
for row in inp.splitlines()
Expand All @@ -38,7 +37,7 @@ def find_path(self, inp: str, *, is_part_two: bool) -> int:
max_dir_traversed = 10 if is_part_two else 3

while to_check:
heat, *set_entry = heappop(to_check)
total_heat, *set_entry = heappop(to_check)
dir_traversed, (row, col), (row_incr, col_incr) = set_entry

if (
Expand All @@ -48,7 +47,7 @@ def find_path(self, inp: str, *, is_part_two: bool) -> int:
# part 2: we need to have not turned for at least 4 blocks before we can end
and (dir_traversed >= 4 if is_part_two else True)
):
return heat
return total_heat

if (set_entry := tuple(set_entry)) not in traversed:
directions = []
Expand Down Expand Up @@ -88,7 +87,7 @@ def find_path(self, inp: str, *, is_part_two: bool) -> int:
to_check,
(
# add current heat of block
heat + grid[new_row][new_col],
total_heat + grid[new_row][new_col],
# if we've changed directions, reset `dir_traversed` counter to 1 (fresh direction)
# else we add to the counter
1 if changed_directions else dir_traversed + 1,
Expand All @@ -100,10 +99,10 @@ def find_path(self, inp: str, *, is_part_two: bool) -> int:
raise ValueError('Failed to traverse path')

def part_one(self, inp: str) -> int:
return self.find_path(inp, is_part_two=False)
return self._find_path(inp, is_part_two=False)

def part_two(self, inp: str) -> int:
return self.find_path(inp, is_part_two=True)
return self._find_path(inp, is_part_two=True)

def run(self, inp: str) -> None:
print('Part 1:', p1 := self.part_one(inp))
Expand Down
144 changes: 144 additions & 0 deletions aoc-py/solutions/day18.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
from __future__ import annotations
"""
Day 18: Lavaduct Lagoon
https://adventofcode.com/2023/day/18
"""
__all__ = ('Day18',)

from typing import (
Iterator,
ClassVar,
TypeAlias,
TYPE_CHECKING,
)

from ..solution import Solution

if TYPE_CHECKING:
Points: TypeAlias = list[tuple[int, int]]
Entry: TypeAlias = tuple[int, tuple[int, int]]

class Day18(Solution):
NAME: ClassVar[str] = 'Lavaduct Lagoon'

def _parse_p1(self, line: str) -> Entry:
"""Parses each line from Part 1:
- ignores the hexadecimal number
(i.e.) 'R 6 (#70c710)' -> (6, (1, 0))
"""
direction, dist, *_ = line.split()
return (
int(dist),
{
'U': (0, 1),
'D': (0, -1),
'L': (-1, 0),
'R': (1, 0),
}.get(direction, (0, 0))
)

def _parse_p2(self, line: str) -> Entry:
"""Parses each line from Part 2:
- ignores the provided direction and distance
the direction and distance are now parsed from the hexadecimal
(i.e.) 'R 6 (#70c710)'
direction = last digit (in this case 0)
where 0 -> R | 1 -> D | 2 -> L | 3 -> U
distance = first 5 digits (in this case 70c71)
(converted to decimal (base 10) from hex (base 16))
-> (461937, (1, 0))
"""
*_, hexcode = line.split()
hexcode = hexcode.strip('(#)')
dist, direction = hexcode[:-1], hexcode[-1]
return (
int(dist, 16),
[
(1, 0),
(0, -1),
(-1, 0),
(0, 1),
][int(direction)]
)

def _shoelace(self, points: Points) -> int:
"""Gets the interior enclosed area of a polygon
given the points in clockwise order
Since the way our data is structured, the coordinates represent the tiles themselves and not points
the shoelace formula will return only the cartesian internal area
this value is not of use to us at all directly as it works for cartesian polygons
(it has no meaning in this application)
but it is only useful to input into `Pick's Theorem` as `A`
<https://en.wikipedia.org/wiki/Shoelace_formula>
"""
return abs(
sum(
x1 * y2 - x2 * y1
for (x1, y1), (x2, y2) in zip(
points,
[*points[1:], points[0]]
)
) // 2
)

def _get_area(self, data: Iterator[Entry]) -> int:
"""Maps out the points (in cartesian) of the lagoon polygon starting at (0, 0)
and following each instruction given the distance and direction to travel in
Uses the Shoelace Formula + Picks's Theorem
<https://en.wikipedia.org/wiki/Pick%27s_theorem>
"""
# points of the polygon (lagoon) in clockwise order starting at the origin
points: Points = [(0, 0)]
# the total perimeter of the shape
# (amount of tiles dug/traversed)
perimeter = 0
for dist, (dir_x, dir_y) in data:
last_x, last_y = points[-1]

points.append((
last_x + dir_x * dist,
last_y + dir_y * dist,
))
perimeter += dist

# Pick's theorem
# ==============
# A = i + b/2 - 1
# where A = the area of the polygon
# i = # of interior lattice points
# b = # of boundary lattice points
# rearrange:
# A - b/2 + 1 = i
# + b both sides
# A + b/2 + 1 = i + b
# what we want is i + b actually and not A as our result
# A = what we get from applying the `shoelace formula``
# b = `perimeter` (which is the same as # of interior boundary lattice points in this case)
# i + b = internal lattice + boundary lattice = the desired output for us
#
return self._shoelace(points) + perimeter // 2 + 1

def part_one(self, inp: str) -> int:
return self._get_area(
map(self._parse_p1, inp.splitlines())
)

def part_two(self, inp: str) -> int:
return self._get_area(
map(self._parse_p2, inp.splitlines())
)

def run(self, inp: str) -> None:
print('Part 1:', p1 := self.part_one(inp))
print('Part 2:', p2 := self.part_two(inp))

assert p1 == 61865
assert p2 == 40343619199142
Loading

0 comments on commit 77cdd44

Please sign in to comment.