diff --git a/advent/common/position.py b/advent/common/position.py index 327852a..cb33ea2 100644 --- a/advent/common/position.py +++ b/advent/common/position.py @@ -12,7 +12,7 @@ class Position: @classmethod def splat(cls, value: int) -> Position: """ Creates a Position with two equal values """ - return Position(value, value) + return cls(value, value) def __str__(self) -> str: return f"({self.x}, {self.y})" @@ -79,10 +79,9 @@ class Position: def is_within(self, top_left: Position, bottom_right: Position) -> bool: """ Checks if this point is within the rectangle spanned by the given positions. - bottom_right is considered to be the first point outside the spanned rectangle. Behavior is undefined if top_left and bottom_right are not in the correct order. """ - return top_left.x <= self.x < bottom_right.x and top_left.y <= self.y < bottom_right.y + return top_left.x <= self.x <= bottom_right.x and top_left.y <= self.y <= bottom_right.y def taxicab_distance(self, other: Position | None = None) -> int: """ @@ -94,6 +93,29 @@ class Position: else: return abs(self.x - other.x) + abs(self.y - other.y) + def component_min(self, *others: Position) -> Position: + best = self + for next in others: + best = Position(min(best.x, next.x), min(best.y, next.y)) + if best.x <= next.x and best.y <= next.y: + pass + elif best.x >= next.x and best.y >= next.y: + best = next + else: + best = Position(min(best.x, next.x), min(best.y, next.y)) + return best + + def component_max(self, *others: Position) -> Position: + best = self + for next in others: + if best.x >= next.x and best.y >= next.y: + pass + elif best.x <= next.x and best.y <= next.y: + best = next + else: + best = Position(max(best.x, next.x), max(best.y, next.y)) + return best + ORIGIN = Position.splat(0) UNIT_X = Position(1, 0) diff --git a/advent/days/day12/solution.py b/advent/days/day12/solution.py index 576afd6..fb6e58a 100644 --- a/advent/days/day12/solution.py +++ b/advent/days/day12/solution.py @@ -27,7 +27,7 @@ class Map: map = list(input) width = len(map[0]) height = len(map) - return Map(map, Position(width, height)) + return Map(map, Position(width - 1, height - 1)) def can_climb(self, *, from_pos: Position, to_pos: Position) -> bool: """ Checks if one gan walk from the elevation at from_pos to the elevation at to_pos """ diff --git a/advent/days/day23/data/expected01_20.txt b/advent/days/day23/data/expected01_20.txt new file mode 100644 index 0000000..db97d04 --- /dev/null +++ b/advent/days/day23/data/expected01_20.txt @@ -0,0 +1,12 @@ +.......#...... +....#......#.. +..#.....#..... +......#....... +...#....#.#..# +#............. +....#.....#... +..#.....#..... +....#.#....#.. +.........#.... +....#......#.. +.......#...... \ No newline at end of file diff --git a/advent/days/day23/solution.py b/advent/days/day23/solution.py index fcf96a7..a401468 100644 --- a/advent/days/day23/solution.py +++ b/advent/days/day23/solution.py @@ -1,6 +1,6 @@ from __future__ import annotations from dataclasses import dataclass -from enum import Enum, auto +from enum import IntEnum from itertools import count, cycle from typing import Iterator @@ -24,96 +24,106 @@ def part2(lines: Iterator[str]) -> int: return result -class Direction(Enum): - North = auto() - South = auto() - West = auto() - East = auto() +class Direction(IntEnum): + North = 0 + South = 1 + West = 2 + East = 3 def next(self) -> Direction: + return Direction((self + 1) % 4) + + def walk(self, position: Position) -> Position: match self: - case Direction.North: return Direction.South - case Direction.South: return Direction.West - case Direction.West: return Direction.East - case Direction.East: return Direction.North - - -@dataclass(slots=True, frozen=True) -class ElfPosition(Position): - def get_adjacent(self, direction: Direction) -> Iterator[ElfPosition]: - match direction: - case Direction.North: - yield ElfPosition(self.x - 1, self.y - 1) - yield ElfPosition(self.x, self.y - 1) - yield ElfPosition(self.x + 1, self.y - 1) - case Direction.South: - yield ElfPosition(self.x - 1, self.y + 1) - yield ElfPosition(self.x, self.y + 1) - yield ElfPosition(self.x + 1, self.y + 1) - case Direction.West: - yield ElfPosition(self.x - 1, self.y - 1) - yield ElfPosition(self.x - 1, self.y) - yield ElfPosition(self.x - 1, self.y + 1) - case Direction.East: - yield ElfPosition(self.x + 1, self.y - 1) - yield ElfPosition(self.x + 1, self.y) - yield ElfPosition(self.x + 1, self.y + 1) - - def get_all_adjacent(self) -> Iterator[ElfPosition]: - for y in range(-1, 2): - for x in range(-1, 2): - if x != 0 or y != 0: - yield ElfPosition(self.x + x, self.y + y) - - def walk(self, direction: Direction) -> ElfPosition: - match direction: - case Direction.North: return ElfPosition(self.x, self.y - 1) - case Direction.South: return ElfPosition(self.x, self.y + 1) - case Direction.West: return ElfPosition(self.x - 1, self.y) - case Direction.East: return ElfPosition(self.x + 1, self.y) - - def min(self, other: ElfPosition) -> ElfPosition: - return ElfPosition(min(self.x, other.x), min(self.y, other.y)) - - def max(self, other: ElfPosition) -> ElfPosition: - return ElfPosition(max(self.x, other.x), max(self.y, other.y)) + case Direction.North: return Position(position.x, position.y - 1) + case Direction.South: return Position(position.x, position.y + 1) + case Direction.West: return Position(position.x - 1, position.y) + case Direction.East: return Position(position.x + 1, position.y) @dataclass(slots=True) class Ground: - map: set[ElfPosition] + map: set[Position] def __str__(self) -> str: min_pos, max_pos = self.extent() result = "" for y in range(min_pos.y, max_pos.y + 1): for x in range(min_pos.x, max_pos.x + 1): - if ElfPosition(x, y) in self.map: + if Position(x, y) in self.map: result += '#' else: result += '.' result += '\n' return result[:-1] + def count_adjacent(self, elves: dict[Position, int], + position: Position) -> list[Direction] | None: + north = False + south = False + west = False + east = False + + if (position + Position(-1, -1)) in elves: + north = True + west = True + if (position + Position(1, 1)) in elves: + south = True + east = True + + if north is False: + north = (position + Position(0, -1)) in elves + if south is False: + south = (position + Position(0, 1)) in elves + if west is False: + west = (position + Position(-1, 0)) in elves + if east is False: + east = (position + Position(1, 0)) in elves + + if north is False or east is False: + if (position + Position(1, -1)) in elves: + north = True + east = True + if south is False or west is False: + if (position + Position(-1, 1)) in elves: + south = True + west = True + + if north == south == east == west: + return None + + adjacent: list[Direction] = [] + if north: + adjacent.append(Direction.North) + if south: + adjacent.append(Direction.South) + if west: + adjacent.append(Direction.West) + if east: + adjacent.append(Direction.East) + + return adjacent + def count_empty(self) -> int: min_pos, max_pos = self.extent() return (max_pos.x - min_pos.x + 1) * (max_pos.y - min_pos.y + 1) - len(self.map) @classmethod def parse(cls, lines: Iterator[str]) -> Ground: - map: set[ElfPosition] = set() + map: set[Position] = set() for y, line in enumerate(lines): for x, tile in enumerate(line): if tile == '#': - map.add(ElfPosition(x, y)) + map.add(Position(x, y)) return Ground(map) - def extent(self) -> tuple[ElfPosition, ElfPosition]: - min_pos = next(iter(self.map)) + def extent(self) -> tuple[Position, Position]: + it = iter(self.map) + min_pos = next(it) max_pos = min_pos - for elf in self.map: - min_pos = min_pos.min(elf) - max_pos = max_pos.max(elf) + for elf in it: + min_pos = min_pos.component_min(elf) + max_pos = max_pos.component_max(elf) return min_pos, max_pos def rounds(self, number: int | None) -> int | None: @@ -123,37 +133,53 @@ class Ground: else: it = range(1, number + 1) + elves = {position: -1 for position in self.map} + + min_moved, max_moved = self.extent() + for n in it: + min_moved = min_moved + Position(-2, -2) + max_moved = max_moved + Position(2, 2) start = next(start_dispenser) - target_map: dict[ElfPosition, ElfPosition] = {} - target_count: dict[ElfPosition, int] = {} - for elf in self.map: - if all(pos not in self.map for pos in elf.get_all_adjacent()): + target_map: dict[Position, Position] = {} + for from_pos, last_moved in elves.items(): + if last_moved + 4 <= n: + if not from_pos.is_within(min_moved, max_moved): + continue + + adjacent = self.count_adjacent(elves, from_pos) + if adjacent is None: continue - check_direction = start - found = False - while not found: - found = True - for check in elf.get_adjacent(check_direction): - if check in self.map: - found = False - break - - if not found: - check_direction = check_direction.next() - if check_direction == start: - found = True + next_direction = start + while True: + if next_direction in adjacent: + next_direction = next_direction.next() else: - target = elf.walk(check_direction) - target_map[elf] = target - target_count[target] = target_count.get(target, 0) + 1 + target = next_direction.walk(from_pos) + if target not in target_map: + target_map[target] = from_pos + else: + del target_map[target] + break changed = False - for from_pos, to_pos in target_map.items(): - if target_count[to_pos] == 1: - changed = True - self.map.remove(from_pos) - self.map.add(to_pos) + first = True + for to_pos, from_pos in target_map.items(): + changed = True + del elves[from_pos] + elves[to_pos] = n + if first: + max_moved = to_pos + min_moved = to_pos + first = False + else: + max_moved = max_moved.component_max(to_pos, from_pos) + min_moved = min_moved.component_min(to_pos, from_pos) + if not changed: + self.map = {elf for elf in elves} return n + + self.map = {elf for elf in elves} + return None