day24 finished
This commit is contained in:
parent
db1d74d201
commit
b5f193ff3c
6 changed files with 419 additions and 0 deletions
336
advent/days/day24/solution.py
Normal file
336
advent/days/day24/solution.py
Normal file
|
|
@ -0,0 +1,336 @@
|
|||
from __future__ import annotations
|
||||
from dataclasses import dataclass, field
|
||||
from enum import IntEnum
|
||||
from queue import PriorityQueue
|
||||
|
||||
from typing import Iterator
|
||||
|
||||
|
||||
day_num = 24
|
||||
|
||||
|
||||
def part1(lines: Iterator[str]) -> int:
|
||||
valley = Valley.parse(lines)
|
||||
return valley.find_way(1)
|
||||
|
||||
|
||||
def part2(lines: Iterator[str]) -> int:
|
||||
valley = Valley.parse(lines)
|
||||
return valley.find_way(3)
|
||||
|
||||
|
||||
def gcd(num1: int, num2: int) -> int:
|
||||
assert num1 >= 0 and num2 >= 0
|
||||
if num1 == 0 or num2 == 0:
|
||||
return 0
|
||||
while num2 != 0:
|
||||
num1, num2 = num2, num1 % num2
|
||||
return num1
|
||||
|
||||
|
||||
def lcm(num1: int, num2: int) -> int:
|
||||
return num1 * num2 // gcd(num1, num2)
|
||||
|
||||
|
||||
Position = tuple[int, int]
|
||||
BlizList = list[Position]
|
||||
BlizTuple = tuple[BlizList, BlizList, BlizList, BlizList]
|
||||
|
||||
|
||||
class Direction(IntEnum):
|
||||
East = 0
|
||||
North = 1
|
||||
West = 2
|
||||
South = 3
|
||||
|
||||
@classmethod
|
||||
def create(cls, char: str) -> Direction:
|
||||
match char:
|
||||
case '>': return Direction.East
|
||||
case '^': return Direction.North
|
||||
case '<': return Direction.West
|
||||
case 'v': return Direction.South
|
||||
case _: raise Exception("Illegal Direction")
|
||||
|
||||
def position(self) -> Position:
|
||||
match self:
|
||||
case Direction.East: return 1, 0
|
||||
case Direction.North: return 0, -1
|
||||
case Direction.West: return -1, 0
|
||||
case Direction.South: return 0, 1
|
||||
|
||||
@property
|
||||
def char(self) -> str:
|
||||
match self:
|
||||
case Direction.East: return ">"
|
||||
case Direction.North: return "^"
|
||||
case Direction.West: return "<"
|
||||
case Direction.South: return "v"
|
||||
|
||||
|
||||
@dataclass(slots=True, frozen=True)
|
||||
class Weather:
|
||||
blizzards: list[dict[Position, str]] = field(repr=False)
|
||||
extent: Position
|
||||
repeat: int
|
||||
|
||||
def print(self, time: int) -> list[str]:
|
||||
current = self.blizzards[self.normal_time(time)]
|
||||
lines: list[str] = []
|
||||
for row in range(self.extent[1]):
|
||||
line = ""
|
||||
for col in range(self.extent[0]):
|
||||
if (char := current.get((col, row))) is not None:
|
||||
line += char
|
||||
else:
|
||||
line += '.'
|
||||
lines.append(line)
|
||||
return lines
|
||||
|
||||
def normal_time(self, time: int) -> int:
|
||||
return time % self.repeat
|
||||
|
||||
def get(self, time: int) -> dict[Position, str]:
|
||||
return self.blizzards[time % self.repeat]
|
||||
|
||||
@classmethod
|
||||
def predict_weather(cls, blizzards: BlizTuple, extent: Position) -> Weather:
|
||||
repeat = lcm(extent[0], extent[1])
|
||||
weather: list[dict[Position, str]] = [Weather.create_dict(blizzards)]
|
||||
for _ in range(repeat - 1):
|
||||
blizzards = Weather.progress_blizzards(blizzards, extent)
|
||||
weather.append(Weather.create_dict(blizzards))
|
||||
|
||||
return Weather(weather, extent, repeat)
|
||||
|
||||
@classmethod
|
||||
def move_east(cls, blizzards: BlizList, extent: Position) -> BlizList:
|
||||
add = Direction.East.position()
|
||||
result: BlizList = []
|
||||
for pos in blizzards:
|
||||
next_pos = pos[0] + add[0], pos[1] + add[1]
|
||||
if next_pos[0] >= extent[0]:
|
||||
next_pos = 0, next_pos[1]
|
||||
result.append(next_pos)
|
||||
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def move_west(cls, blizzards: BlizList, extent: Position) -> BlizList:
|
||||
add = Direction.West.position()
|
||||
result: BlizList = []
|
||||
for pos in blizzards:
|
||||
next_pos = pos[0] + add[0], pos[1] + add[1]
|
||||
if next_pos[0] < 0:
|
||||
next_pos = extent[0] - 1, next_pos[1]
|
||||
result.append(next_pos)
|
||||
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def move_south(cls, blizzards: BlizList, extent: Position) -> BlizList:
|
||||
add = Direction.South.position()
|
||||
result: BlizList = []
|
||||
for pos in blizzards:
|
||||
next_pos = pos[0] + add[0], pos[1] + add[1]
|
||||
if next_pos[1] >= extent[1]:
|
||||
next_pos = next_pos[0], 0
|
||||
result.append(next_pos)
|
||||
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def move_north(cls, blizzards: BlizList, extent: Position) -> BlizList:
|
||||
add = Direction.North.position()
|
||||
result: BlizList = []
|
||||
for pos in blizzards:
|
||||
next_pos = pos[0] + add[0], pos[1] + add[1]
|
||||
if next_pos[1] < 0:
|
||||
next_pos = next_pos[0], extent[1] - 1
|
||||
result.append(next_pos)
|
||||
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def _add_list(cls, map: dict[Position, str], lst: BlizList, char: str) -> dict[Position, str]:
|
||||
for position in lst:
|
||||
match map.get(position):
|
||||
case None: map[position] = char
|
||||
case '2': map[position] = '3'
|
||||
case '3': map[position] = '4'
|
||||
case _: map[position] = '2'
|
||||
return map
|
||||
|
||||
@classmethod
|
||||
def create_dict(cls, blizzards: BlizTuple) -> dict[Position, str]:
|
||||
map = Weather._add_list({}, blizzards[0], Direction.East.char)
|
||||
map = Weather._add_list(map, blizzards[1], Direction.North.char)
|
||||
map = Weather._add_list(map, blizzards[2], Direction.West.char)
|
||||
return Weather._add_list(map, blizzards[3], Direction.South.char)
|
||||
|
||||
@classmethod
|
||||
def progress_blizzards(cls, blizzards: BlizTuple, extent: Position) -> BlizTuple:
|
||||
return (
|
||||
Weather.move_east(blizzards[0], extent),
|
||||
Weather.move_north(blizzards[1], extent),
|
||||
Weather.move_west(blizzards[2], extent),
|
||||
Weather.move_south(blizzards[3], extent),
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, frozen=True)
|
||||
class Valley:
|
||||
weather: Weather = field(repr=False)
|
||||
extent: Position
|
||||
start: Position
|
||||
exit: Position
|
||||
|
||||
@classmethod
|
||||
def append(cls, blizzards: BlizTuple, direction: Direction, position: Position) -> BlizTuple:
|
||||
return tuple(
|
||||
directed if num != direction else directed + [position]
|
||||
for num, directed in enumerate(blizzards)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _get_wallbreak(cls, lines: str) -> int:
|
||||
for col, tile in enumerate(lines[1:]):
|
||||
if tile == '.':
|
||||
return col
|
||||
raise Exception("No break in the wall")
|
||||
|
||||
@classmethod
|
||||
def parse_line(cls, blizzards: BlizTuple, line: str, row: int) -> BlizTuple:
|
||||
for col, char in enumerate(line[1:]):
|
||||
match char:
|
||||
case '#':
|
||||
return blizzards
|
||||
case '>' | '^' | '<' | 'v':
|
||||
blizzards = Valley.append(blizzards, Direction.create(char), (col, row))
|
||||
case '.':
|
||||
pass
|
||||
case _:
|
||||
raise Exception("Unknown char: {char}")
|
||||
|
||||
raise Exception("line not terminated by wall")
|
||||
|
||||
@classmethod
|
||||
def parse(cls, lines: Iterator[str]) -> Valley:
|
||||
first_line = next(lines)
|
||||
start_col = Valley._get_wallbreak(first_line)
|
||||
width = len(first_line) - 2
|
||||
blizzards: BlizTuple = [], [], [], []
|
||||
for row, line in enumerate(lines):
|
||||
if line.startswith("##"):
|
||||
end_col = Valley._get_wallbreak(line)
|
||||
extent = width, row
|
||||
return Valley(Weather.predict_weather(blizzards, extent), extent,
|
||||
(start_col, -1), (end_col, row))
|
||||
else:
|
||||
blizzards = Valley.parse_line(blizzards, line, row)
|
||||
assert False, "Unreachable"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return '\n'.join(self.print(0))
|
||||
|
||||
def print(self, time: int) -> list[str]:
|
||||
first = '#' + ('#' * self.start[0]) + '.' + ('#' * (self.extent[0] - self.start[0]))
|
||||
last = '#' + ('#' * self.exit[0]) + '.' + ('#' * (self.extent[0] - self.exit[0]))
|
||||
lines = self.weather.print(time)
|
||||
lines = [first] + ['#' + line + '#' for line in lines] + [last]
|
||||
return lines
|
||||
|
||||
def find_way(self, rounds: int) -> int:
|
||||
queue: PriorityQueue[Step] = PriorityQueue()
|
||||
queue.put(Step(
|
||||
position=self.start,
|
||||
time=0,
|
||||
round=0,
|
||||
valley=self,
|
||||
start=self.start,
|
||||
target=self.exit))
|
||||
reached: set[tuple[Position, int, int]] = set()
|
||||
while not queue.empty():
|
||||
current = queue.get()
|
||||
if current.round == rounds:
|
||||
return current.time
|
||||
|
||||
normal_time = self.weather.normal_time(current.time)
|
||||
if (current.position, normal_time, current.round) in reached:
|
||||
continue
|
||||
|
||||
reached.add((current.position, normal_time, current.round))
|
||||
for next in current.possible_moves():
|
||||
queue.put(next)
|
||||
|
||||
raise Exception("No path found")
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class Step:
|
||||
position: Position
|
||||
time: int
|
||||
round: int
|
||||
start: Position
|
||||
target: Position
|
||||
valley: Valley
|
||||
|
||||
def __lt__(self, other: Step) -> bool:
|
||||
return self.time < other.time
|
||||
|
||||
def __str__(self) -> str:
|
||||
return '\n'.join(self.print())
|
||||
|
||||
def print(self) -> list[str]:
|
||||
lines = self.valley.print(self.time)
|
||||
|
||||
line = lines[self.position[1] + 1]
|
||||
lines[self.position[1] + 1] = line[:self.position[0] + 1] + \
|
||||
'E' + line[self.position[0] + 2:]
|
||||
|
||||
return lines
|
||||
|
||||
def reach_target(self) -> Step:
|
||||
path = Step(position=self.target,
|
||||
time=self.time + 1,
|
||||
round=self.round + 1,
|
||||
valley=self.valley,
|
||||
start=self.target,
|
||||
target=self.start,
|
||||
)
|
||||
return path
|
||||
|
||||
def move(self, position: Position) -> Step:
|
||||
return Step(position=position,
|
||||
time=self.time + 1,
|
||||
round=self.round,
|
||||
valley=self.valley,
|
||||
start=self.start,
|
||||
target=self.target,
|
||||
)
|
||||
|
||||
def wait(self) -> Step:
|
||||
return Step(position=self.position,
|
||||
time=self.time + 1,
|
||||
round=self.round,
|
||||
valley=self.valley,
|
||||
start=self.start,
|
||||
target=self.target,
|
||||
)
|
||||
|
||||
def possible_moves(self) -> Iterator[Step]:
|
||||
impassable = self.valley.weather.get(self.time + 1)
|
||||
|
||||
if self.position not in impassable:
|
||||
yield self.wait()
|
||||
|
||||
for dir in Direction:
|
||||
add = dir.position()
|
||||
next_position = self.position[0] + add[0], self.position[1] + add[1]
|
||||
if next_position == self.target:
|
||||
yield self.reach_target()
|
||||
|
||||
elif (0 <= next_position[0] < self.valley.extent[0]
|
||||
and 0 <= next_position[1] < self.valley.extent[1]):
|
||||
if next_position not in impassable:
|
||||
yield self.move(next_position)
|
||||
Loading…
Add table
Add a link
Reference in a new issue