Skip to content

Commit

Permalink
day 22
Browse files Browse the repository at this point in the history
  • Loading branch information
Tom-the-Bomb committed Dec 27, 2023
1 parent 876ff3c commit eda7d95
Show file tree
Hide file tree
Showing 8 changed files with 2,864 additions and 2 deletions.
8 changes: 7 additions & 1 deletion aoc-py/solutions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
'Day17',
'Day18',
'Day19',
'Day20',
'Day21',
'Day22',
'Day25',
)

from .day1 import Day1
Expand All @@ -42,6 +46,8 @@
from .day19 import Day19
from .day20 import Day20
from .day21 import Day21
from .day22 import Day22
from .day25 import Day25

from ..solution import Solution

Expand All @@ -50,7 +56,7 @@
Day6, Day7, Day8, Day9, Day10,
Day11, Day12, Day13, Day14, Day15,
Day16, Day17, Day18, Day19, Day20,
Day21,
Day21, Day22, Day25,
)

del Solution
4 changes: 3 additions & 1 deletion aoc-py/solutions/day21.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,11 @@ def traverse(
# i.e. when we are in th initial, given grid:
# `n_row_wraps` and `n_col_wraps` are both `0`
#
# quotient of dividing the current `index` by the number of rows/cols
# quotient of dividing the current `index` by the # of rows/cols
# needs to be stored in `traversed` to allow the set to distinguish
# between coordinates that are the same, but are on different cycles/wraps of the grid
#
# the modulo of the index and the # of rows/cols will give us the index we can use on the gri
new_row_wraps, new_row = divmod(new_row, n_rows)
new_col_wraps, new_col = divmod(new_col, n_cols)
new_row_wraps += n_row_wraps
Expand Down
200 changes: 200 additions & 0 deletions aoc-py/solutions/day22.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
from __future__ import annotations
"""
Day 22: Sand Slabs
https://adventofcode.com/2023/day/22
"""
__all__ = ('Day22',)

from typing import ClassVar
from collections import defaultdict

from ..solution import Solution

class Point:
"""Represents a coordinate/point in 3D space (x, y, z)
A brick contains a pair of these, representing 2 ends of the brick
"""
__slots__ = ('x', 'y', 'z')

def __init__(self, x: int, y: int, z: int) -> None:
self.x = x
self.y = y
self.z = z

@classmethod
def from_str(cls, raw: str) -> Point:
"""Parses from given input"""
x, y, z = raw.split(',')
return cls(int(x), int(y), int(z))

def __repr__(self) -> str:
return f'<{self.__class__.__name__} ({self.x}, {self.y}, {self.z})>'

def __eq__(self, other: Point) -> bool:
return self.x == other.x and self.y == other.y and self.z == other.z

def __ne__(self, other: Point) -> bool:
return not self == other

class Brick:
"""Represents a brick
Stores information about its provided 2 coordinate points
"""
__slots__ = ('bottom', 'top')

def __init__(self, bottom: Point, top: Point) -> None:
self.bottom = bottom
self.top = top

@classmethod
def from_str(cls, raw: str) -> Brick:
"""Parses from given input"""
bottom, top = raw.split('~')
return cls(
Point.from_str(bottom),
Point.from_str(top),
)

@property
def height(self) -> int:
"""Returns the height / thickness of the brick itself, difference in z-values"""
return self.top.z - self.bottom.z

def __repr__(self) -> str:
return f'<{self.__class__.__name__} bottom={self.bottom!r} top={self.top!r}>'

def overlaps_xy(self, other: Brick) -> bool:
"""Checks whether or not the bricks overlap when looking from a birds-eye view
gives us insight into whether or not there even is potential for them to support each other
by telling us whether or not they are above each other
"""
return (
max(self.bottom.x, other.bottom.x) <= min(self.top.x, other.top.x)
and max(self.bottom.y, other.bottom.y) <= min(self.top.y, other.top.y)
)

def __eq__(self, other: Brick) -> bool:
return self.bottom == other.bottom and self.top == other.top

def __ne__(self, other: Brick) -> bool:
return not self == other

def __ge__(self, other: Brick) -> bool:
return self.bottom.z >= other.bottom.z

def __gt__(self, other: Brick) -> bool:
return self.bottom.z > other.bottom.z

def __le__(self, other: Brick) -> bool:
return self.bottom.z <= other.bottom.z

def __lt__(self, other: Brick) -> bool:
return self.bottom.z < other.bottom.z

class Day22(Solution):
NAME: ClassVar[str] = 'Sand Slabs'

def _get_support_mappings(self, inp: str) -> tuple[list[Brick], dict[int, set[int]], dict[int, set[int]]]:
bricks = [Brick.from_str(line) for line in inp.splitlines()]
# sort from lowest-z to highest-z (height)
bricks.sort()

# drop bricks
for i, brick in enumerate(bricks):
z = 1
# all bricks under `brick`
for lower_brick in bricks[:i]:
# `brick` overlaps with `lower_brick` on the x-y plane,
# since `lower_brick` is under `brick`, and they overlap
# (overlap = `lower_brick` can support `brick`)
if brick.overlaps_xy(lower_brick):
# this indicates that `brick` cannot fall any lower than `lower_brick`'s top height + 1
#
# repeat this process for all bricks under `brick` and
# get the maximum height it can fall down to of all the bricks
# giving us where `brick` will fall down to ultimately
z = max(z, lower_brick.top.z + 1)
# `z` represents what the brick's BOTTOM will drop down to
#
# set z-level of the top of the brick to `z` + the height offset
brick.top.z = brick.height + z
# drop the z-level of the brick's bottom to `z`
brick.bottom.z = z
bricks.sort()

# 2 way mapping:
# maps: brick -> what other bricks that brick supports
supports = defaultdict(set)
# maps: brick -> what supports that brick
supported_by = defaultdict(set)

for a, brick in enumerate(bricks):
# all bricks beneath brick `a`
for b, lower_brick in enumerate(bricks[:a]):
if (
# bricks overlap on the x-y plane: potential for support
lower_brick.overlaps_xy(brick)
# the brick's bottom's z-value is exactly equal to the z-value + 1 of the top of the brick below it
# indicating that `lower_brick` is touching/supporting `brick`
# `b` brick is supporting `a`
and brick.bottom.z == lower_brick.top.z + 1
):
supports[b].add(a)
supported_by[a].add(b)
return bricks, supports, supported_by

def part_one(self, inp: str) -> int:
bricks, supports, supported_by = self._get_support_mappings(inp)

return sum(
1
# for all bricks (`b`) check:
for b in range(len(bricks))
# there are more than 1 brick supporting `a`
# indicating there is another brick OTHER than `b` to support `a`
# therefore, we can safely disintegrate `b` as `a` will still be supported
#
# for all bricks (`a`) that `b` supports
if all(len(supported_by[a]) > 1 for a in supports[b])
)

def part_two(self, inp: str) -> int:
bricks, supports, supported_by = self._get_support_mappings(inp)
total = 0

for b in range(len(bricks)):
# all bricks `a` that are SOLELY supported by `b`
# which will also be all the bricks that will fall once `b` disintegrates
#
# 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
]
# bricks that are going to fall
falling = set(to_check + [b])

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 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
return total

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

assert p1 == 459
assert p2 == 75784
44 changes: 44 additions & 0 deletions aoc-py/solutions/day25.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"""
Day 25: Snowverload
https://adventofcode.com/2023/day/25
"""
__all__ = ('Day25',)

from typing import ClassVar

import networkx as nx

from ..solution import Solution

class Day25(Solution):
NAME: ClassVar[str] = 'Snowverload'

def part_one(self, inp: str) -> int:
graph = nx.Graph()

for line in inp.splitlines():
left, right = line.split(':')
for node in right.strip().split():
graph.add_edge(left, node)
graph.add_edge(node, left)

graph.remove_edges_from(
nx.minimum_edge_cut(graph)
)

a, b = nx.connected_components(graph)
return len(a) * len(b)

def part_two(self, _: str) -> None:
"""No part 2 for day 25!
Merry Christmas!
"""

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

assert p1 == 554064
assert p2 is None
Loading

0 comments on commit eda7d95

Please sign in to comment.