diff --git a/aoc_2023/day_18/__init__.py b/aoc_2023/day_18/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/aoc_2023/day_18/a.py b/aoc_2023/day_18/a.py new file mode 100644 index 0000000..67cf186 --- /dev/null +++ b/aoc_2023/day_18/a.py @@ -0,0 +1,34 @@ +from dataclasses import dataclass +from functools import cached_property +from aoc_2023.day_18.common import Instruction, Solver +from aoc_2023.day_18.parser import Parser + + +@dataclass +class Day18PartASolver: + instructions: list[Instruction] + + @property + def solution(self) -> int: + return Solver(self.instructions).solution + + @cached_property + def solver(self) -> Solver: + return Solver(self.instructions) + + +def solve(input: str) -> int: + data = Parser.parse(input) + solver = Day18PartASolver(data) + + return solver.solution + + +def get_solution() -> int: + with open("aoc_2023/day_18/input.txt", "r") as f: + input = f.read() + return solve(input) + + +if __name__ == "__main__": + print(get_solution()) diff --git a/aoc_2023/day_18/b.py b/aoc_2023/day_18/b.py new file mode 100644 index 0000000..f308f2a --- /dev/null +++ b/aoc_2023/day_18/b.py @@ -0,0 +1,54 @@ +from dataclasses import dataclass +from functools import cached_property +from aoc_2023.day_18.common import ( + Instruction, + Solver, +) +from aoc_2023.day_18.parser import Parser + + +@dataclass +class Day18PartBSolver: + wrong_instructions: list[Instruction] + + @property + def solution(self) -> int: + return Solver(self.instructions).solution + + @cached_property + def solver(self) -> Solver: + return Solver(self.instructions) + + @cached_property + def instructions(self) -> list[Instruction]: + return [self.correct_instruction(inst) for inst in self.wrong_instructions] + + def correct_instruction(self, instruction: Instruction) -> Instruction: + dist = instruction.color[:5] + direction = instruction.color[-1] + + DIRECTION_MAP = { + "0": "R", + "1": "D", + "2": "L", + "3": "U", + } + + return Instruction(DIRECTION_MAP[direction], int(dist, 16), instruction.color) + + +def solve(input: str) -> int: + data = Parser.parse(input) + solver = Day18PartBSolver(data) + + return solver.solution + + +def get_solution() -> int: + with open("aoc_2023/day_18/input.txt", "r") as f: + input = f.read() + return solve(input) + + +if __name__ == "__main__": + print(get_solution()) diff --git a/aoc_2023/day_18/common.py b/aoc_2023/day_18/common.py new file mode 100644 index 0000000..d41d9ed --- /dev/null +++ b/aoc_2023/day_18/common.py @@ -0,0 +1,143 @@ +from __future__ import annotations +from dataclasses import dataclass +from functools import cached_property + +from aoc_2023.tools.point import Point + + +@dataclass +class Instruction: + direction: str + length: int + color: str + + +@dataclass(frozen=True) +class VerticalSegment: + x: int + y_range: tuple[int, int] + + def __post_init__(self): + assert self.y_range[0] < self.y_range[1] + + +UP = Point(0, -1) +DOWN = Point(0, 1) +LEFT = Point(-1, 0) +RIGHT = Point(1, 0) + +INSTRUCTION_MAP = { + "U": UP, + "D": DOWN, + "L": LEFT, + "R": RIGHT, +} + + +@dataclass +class Solver: + instructions: list[Instruction] + + @cached_property + def solution(self) -> int: + by_y = self.segments_by_y_range + terminals = set[VerticalSegment]() + output = 0 + + for x in self.xs: + for segment in self.segments_by_x[x]: + if segment not in terminals: + index_by_y = by_y[segment.y_range].index(segment) + end_segment = by_y[segment.y_range][index_by_y + 1] + width = (end_segment.x + 1) - segment.x + assert width > 0 + height = segment.y_range[1] - segment.y_range[0] + assert height > 0 + output += width * height + terminals.add(end_segment) + + output += self.left_len + output += 1 + return output + + @cached_property + def xs(self) -> list[int]: + return sorted({p.x for p in self.points}) + + @cached_property + def ys(self) -> list[int]: + return sorted({p.y for p in self.points}) + + @cached_property + def points(self) -> list[Point]: + pos = Point(0, 0) + output = [pos] + for inst in self.instructions: + move = INSTRUCTION_MAP[inst.direction].multiply(inst.length) + pos = pos.add(move) + output.append(pos) + return output + + @cached_property + def segments_by_x(self) -> dict[int, list[VerticalSegment]]: + output = dict[int, list[VerticalSegment]]() + for segment in self.small_vertical_segments: + if segment.x not in output: + output[segment.x] = [] + output[segment.x].append(segment) + for segments in output.values(): + segments.sort(key=lambda s: s.y_range) + return output + + @cached_property + def segments_by_y_range(self) -> dict[tuple[int, int], list[VerticalSegment]]: + output = dict[tuple[int, int], list[VerticalSegment]]() + for segment in self.small_vertical_segments: + if segment.y_range not in output: + output[segment.y_range] = [] + output[segment.y_range].append(segment) + for segments in output.values(): + segments.sort(key=lambda s: s.x) + return output + + @cached_property + def small_vertical_segments(self) -> list[VerticalSegment]: + output = list[VerticalSegment]() + for large_segment in self.large_vertical_segments: + ys = [ + y + for y in self.ys + if y >= large_segment.y_range[0] and y <= large_segment.y_range[1] + ] + for i in range(1, len(ys)): + output.append( + VerticalSegment( + x=large_segment.x, + y_range=(ys[i - 1], ys[i]), + ) + ) + + return output + + @cached_property + def large_vertical_segments(self) -> list[VerticalSegment]: + pos = Point(0, 0) + output = list[VerticalSegment]() + for inst in self.instructions: + move = INSTRUCTION_MAP[inst.direction].multiply(inst.length) + new_pos = pos.add(move) + if new_pos.x == pos.x: + output.append( + VerticalSegment( + x=new_pos.x, + y_range=(pos.y, new_pos.y) + if pos.y < new_pos.y + else (new_pos.y, pos.y), + ) + ) + pos = new_pos + return output + + @cached_property + def left_len(self) -> int: + return sum(inst.length for inst in self.instructions if inst.direction == "L") diff --git a/aoc_2023/day_18/from_prompt.py b/aoc_2023/day_18/from_prompt.py new file mode 100644 index 0000000..fb89c5c Binary files /dev/null and b/aoc_2023/day_18/from_prompt.py differ diff --git a/aoc_2023/day_18/input.txt b/aoc_2023/day_18/input.txt new file mode 100644 index 0000000..6be505b Binary files /dev/null and b/aoc_2023/day_18/input.txt differ diff --git a/aoc_2023/day_18/parser.py b/aoc_2023/day_18/parser.py new file mode 100644 index 0000000..65caad8 --- /dev/null +++ b/aoc_2023/day_18/parser.py @@ -0,0 +1,14 @@ +from aoc_2023.day_18.common import Instruction + + +class Parser: + @staticmethod + def parse(input: str) -> list[Instruction]: + lines = input.strip().splitlines() + return [Parser.parse_line(line) for line in lines] + + @staticmethod + def parse_line(line: str) -> Instruction: + direction, length, color = line.split() + + return Instruction(direction, int(length), color[2:-1]) diff --git a/tests/test_day_18/__init__.py b/tests/test_day_18/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_day_18/test_a.py b/tests/test_day_18/test_a.py new file mode 100644 index 0000000..47a6ce9 --- /dev/null +++ b/tests/test_day_18/test_a.py @@ -0,0 +1,10 @@ +from aoc_2023.day_18.a import get_solution, solve +from aoc_2023.day_18.from_prompt import SAMPLE_DATA, SAMPLE_SOLUTION_A + + +def test_solve(): + assert solve(SAMPLE_DATA) == SAMPLE_SOLUTION_A + + +def test_my_solution(): + assert get_solution() == 40714 diff --git a/tests/test_day_18/test_b.py b/tests/test_day_18/test_b.py new file mode 100644 index 0000000..b18cc46 --- /dev/null +++ b/tests/test_day_18/test_b.py @@ -0,0 +1,10 @@ +from aoc_2023.day_18.b import get_solution, solve +from aoc_2023.day_18.from_prompt import SAMPLE_DATA, SAMPLE_SOLUTION_B + + +def test_solve(): + assert solve(SAMPLE_DATA) == SAMPLE_SOLUTION_B + + +def test_my_solution(): + assert get_solution() == 129849166997110