day23 faster
This commit is contained in:
parent
bb5524601a
commit
3940c75527
4 changed files with 149 additions and 89 deletions
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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 """
|
||||||
|
|
|
||||||
12
advent/days/day23/data/expected01_20.txt
Normal file
12
advent/days/day23/data/expected01_20.txt
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
.......#......
|
||||||
|
....#......#..
|
||||||
|
..#.....#.....
|
||||||
|
......#.......
|
||||||
|
...#....#.#..#
|
||||||
|
#.............
|
||||||
|
....#.....#...
|
||||||
|
..#.....#.....
|
||||||
|
....#.#....#..
|
||||||
|
.........#....
|
||||||
|
....#......#..
|
||||||
|
.......#......
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue