day23 faster

This commit is contained in:
Ruediger Ludwig 2023-01-21 11:21:08 +01:00
parent bb5524601a
commit 3940c75527
4 changed files with 149 additions and 89 deletions

View file

@ -12,7 +12,7 @@ class Position:
@classmethod @classmethod
def splat(cls, value: int) -> Position: def splat(cls, value: int) -> Position:
""" Creates a Position with two equal values """ """ Creates a Position with two equal values """
return Position(value, value) return cls(value, value)
def __str__(self) -> str: def __str__(self) -> str:
return f"({self.x}, {self.y})" return f"({self.x}, {self.y})"
@ -79,10 +79,9 @@ class Position:
def is_within(self, top_left: Position, bottom_right: Position) -> bool: def is_within(self, top_left: Position, bottom_right: Position) -> bool:
""" """
Checks if this point is within the rectangle spanned by the given positions. 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. 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: def taxicab_distance(self, other: Position | None = None) -> int:
""" """
@ -94,6 +93,29 @@ class Position:
else: else:
return abs(self.x - other.x) + abs(self.y - other.y) 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) ORIGIN = Position.splat(0)
UNIT_X = Position(1, 0) UNIT_X = Position(1, 0)

View file

@ -27,7 +27,7 @@ class Map:
map = list(input) map = list(input)
width = len(map[0]) width = len(map[0])
height = len(map) 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: 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 """ """ Checks if one gan walk from the elevation at from_pos to the elevation at to_pos """

View file

@ -0,0 +1,12 @@
.......#......
....#......#..
..#.....#.....
......#.......
...#....#.#..#
#.............
....#.....#...
..#.....#.....
....#.#....#..
.........#....
....#......#..
.......#......

View file

@ -1,6 +1,6 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum, auto from enum import IntEnum
from itertools import count, cycle from itertools import count, cycle
from typing import Iterator from typing import Iterator
@ -24,96 +24,106 @@ def part2(lines: Iterator[str]) -> int:
return result return result
class Direction(Enum): class Direction(IntEnum):
North = auto() North = 0
South = auto() South = 1
West = auto() West = 2
East = auto() East = 3
def next(self) -> Direction: def next(self) -> Direction:
return Direction((self + 1) % 4)
def walk(self, position: Position) -> Position:
match self: match self:
case Direction.North: return Direction.South case Direction.North: return Position(position.x, position.y - 1)
case Direction.South: return Direction.West case Direction.South: return Position(position.x, position.y + 1)
case Direction.West: return Direction.East case Direction.West: return Position(position.x - 1, position.y)
case Direction.East: return Direction.North case Direction.East: return Position(position.x + 1, position.y)
@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))
@dataclass(slots=True) @dataclass(slots=True)
class Ground: class Ground:
map: set[ElfPosition] map: set[Position]
def __str__(self) -> str: def __str__(self) -> str:
min_pos, max_pos = self.extent() min_pos, max_pos = self.extent()
result = "" result = ""
for y in range(min_pos.y, max_pos.y + 1): for y in range(min_pos.y, max_pos.y + 1):
for x in range(min_pos.x, max_pos.x + 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 += '#' result += '#'
else: else:
result += '.' result += '.'
result += '\n' result += '\n'
return result[:-1] 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: def count_empty(self) -> int:
min_pos, max_pos = self.extent() min_pos, max_pos = self.extent()
return (max_pos.x - min_pos.x + 1) * (max_pos.y - min_pos.y + 1) - len(self.map) return (max_pos.x - min_pos.x + 1) * (max_pos.y - min_pos.y + 1) - len(self.map)
@classmethod @classmethod
def parse(cls, lines: Iterator[str]) -> Ground: def parse(cls, lines: Iterator[str]) -> Ground:
map: set[ElfPosition] = set() map: set[Position] = set()
for y, line in enumerate(lines): for y, line in enumerate(lines):
for x, tile in enumerate(line): for x, tile in enumerate(line):
if tile == '#': if tile == '#':
map.add(ElfPosition(x, y)) map.add(Position(x, y))
return Ground(map) return Ground(map)
def extent(self) -> tuple[ElfPosition, ElfPosition]: def extent(self) -> tuple[Position, Position]:
min_pos = next(iter(self.map)) it = iter(self.map)
min_pos = next(it)
max_pos = min_pos max_pos = min_pos
for elf in self.map: for elf in it:
min_pos = min_pos.min(elf) min_pos = min_pos.component_min(elf)
max_pos = max_pos.max(elf) max_pos = max_pos.component_max(elf)
return min_pos, max_pos return min_pos, max_pos
def rounds(self, number: int | None) -> int | None: def rounds(self, number: int | None) -> int | None:
@ -123,37 +133,53 @@ class Ground:
else: else:
it = range(1, number + 1) it = range(1, number + 1)
elves = {position: -1 for position in self.map}
min_moved, max_moved = self.extent()
for n in it: for n in it:
min_moved = min_moved + Position(-2, -2)
max_moved = max_moved + Position(2, 2)
start = next(start_dispenser) start = next(start_dispenser)
target_map: dict[ElfPosition, ElfPosition] = {} target_map: dict[Position, Position] = {}
target_count: dict[ElfPosition, int] = {} for from_pos, last_moved in elves.items():
for elf in self.map: if last_moved + 4 <= n:
if all(pos not in self.map for pos in elf.get_all_adjacent()): if not from_pos.is_within(min_moved, max_moved):
continue continue
check_direction = start adjacent = self.count_adjacent(elves, from_pos)
found = False if adjacent is None:
while not found: continue
found = True
for check in elf.get_adjacent(check_direction): next_direction = start
if check in self.map: while True:
found = False if next_direction in adjacent:
next_direction = next_direction.next()
else:
target = next_direction.walk(from_pos)
if target not in target_map:
target_map[target] = from_pos
else:
del target_map[target]
break break
if not found:
check_direction = check_direction.next()
if check_direction == start:
found = True
else:
target = elf.walk(check_direction)
target_map[elf] = target
target_count[target] = target_count.get(target, 0) + 1
changed = False changed = False
for from_pos, to_pos in target_map.items(): first = True
if target_count[to_pos] == 1: for to_pos, from_pos in target_map.items():
changed = True changed = True
self.map.remove(from_pos) del elves[from_pos]
self.map.add(to_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: if not changed:
self.map = {elf for elf in elves}
return n return n
self.map = {elf for elf in elves}
return None