2021: d23: ex1: add solution
This commit is contained in:
parent
130e417c62
commit
05fdbcc303
200
2021/d23/ex1/ex1.py
Executable file
200
2021/d23/ex1/ex1.py
Executable file
|
@ -0,0 +1,200 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import enum
|
||||
import functools
|
||||
import sys
|
||||
from typing import Iterator, List, NamedTuple, Optional, Tuple, cast
|
||||
|
||||
|
||||
class Point(NamedTuple):
|
||||
x: int
|
||||
y: int
|
||||
|
||||
|
||||
class Amphipod(enum.IntEnum):
|
||||
A = 0
|
||||
B = 1
|
||||
C = 2
|
||||
D = 3
|
||||
|
||||
|
||||
class Direction(enum.IntEnum):
|
||||
ALLEY = 0
|
||||
ROOM = 1
|
||||
|
||||
|
||||
# 7-length tuple, but easier for mypy in a variadic type
|
||||
Alley = Tuple[Optional[Amphipod], ...]
|
||||
|
||||
# Actually variadic tuple, instead of a list, for memoization purposes
|
||||
Room = Tuple[Amphipod, ...]
|
||||
|
||||
# 4-length tuple, but easier for mypy in a variadic type
|
||||
Rooms = Tuple[Room, ...]
|
||||
|
||||
|
||||
class Board(NamedTuple):
|
||||
alley: Alley
|
||||
rooms: Rooms
|
||||
|
||||
|
||||
class Move(NamedTuple):
|
||||
cost: int
|
||||
new_board: Board
|
||||
|
||||
|
||||
FUEL_COST = {
|
||||
Amphipod.A: 1,
|
||||
Amphipod.B: 10,
|
||||
Amphipod.C: 100,
|
||||
Amphipod.D: 1000,
|
||||
}
|
||||
|
||||
DISTANCE = [
|
||||
# From room 1
|
||||
(2, 1, 1, 3, 5, 7, 8),
|
||||
# From room 2
|
||||
(4, 3, 1, 1, 3, 5, 6),
|
||||
# From room 3
|
||||
(6, 5, 3, 1, 1, 3, 4),
|
||||
# From room 4
|
||||
(8, 7, 5, 3, 1, 1, 2),
|
||||
]
|
||||
|
||||
AMPHIPOD_FROM_STRING = {
|
||||
"A": Amphipod.A,
|
||||
"B": Amphipod.B,
|
||||
"C": Amphipod.C,
|
||||
"D": Amphipod.D,
|
||||
}
|
||||
|
||||
ROOM_SIZE = 2
|
||||
|
||||
|
||||
def solve(input: List[str]) -> int:
|
||||
def parse() -> Board:
|
||||
alley: Alley = (None,) * 7
|
||||
rooms: Rooms = ()
|
||||
for i in (3, 5, 7, 9):
|
||||
room: Room = tuple(
|
||||
AMPHIPOD_FROM_STRING[input[j][i]] for j in range(2, 3 + 1)
|
||||
)
|
||||
rooms = rooms + (room,)
|
||||
|
||||
return Board(alley, rooms)
|
||||
|
||||
def room_is_solved(board: Board, amphipod: Amphipod) -> bool:
|
||||
room = board.rooms[amphipod]
|
||||
return len(room) == ROOM_SIZE and all(a == amphipod for a in room)
|
||||
|
||||
def board_is_solved(board: Board) -> bool:
|
||||
return all(room_is_solved(board, Amphipod(i)) for i in range(len(board.rooms)))
|
||||
|
||||
def move_cost(
|
||||
board: Board, room: int, alley_spot: int, direction: Direction
|
||||
) -> Optional[int]:
|
||||
# Going left-to-right, or right-to-left
|
||||
if room < (alley_spot - 1):
|
||||
alley_start = room + 2
|
||||
# Look at the end spot if we're going to the alley, not if we come from there
|
||||
alley_end = alley_spot + (1 - direction)
|
||||
else:
|
||||
# Look at the first spot if we're going to the alley, not if we come from there
|
||||
alley_start = alley_spot + direction
|
||||
alley_end = room + 2
|
||||
|
||||
# Is there any obstacle in the way
|
||||
if any(spot is not None for spot in board.alley[alley_start:alley_end]):
|
||||
return None
|
||||
|
||||
amphipod = (
|
||||
board.alley[alley_spot]
|
||||
if direction == Direction.ROOM
|
||||
else board.rooms[room][0]
|
||||
)
|
||||
assert amphipod is not None # Sanity check
|
||||
|
||||
return FUEL_COST[amphipod] * (
|
||||
DISTANCE[room][alley_spot] + direction + ROOM_SIZE - len(board.rooms[room])
|
||||
)
|
||||
|
||||
# Yes this returns a 0-or-1 length iterator, but it's practical for `moves`
|
||||
def alley_moves_for(board: Board, i: int) -> Iterator[Move]:
|
||||
# Return early if we're trying to move out of an empty spot
|
||||
spot = board.alley[i]
|
||||
if spot is None:
|
||||
return
|
||||
|
||||
# Can't yet move to the target room if any amphipod is out of place there
|
||||
if any(other != spot for other in board.rooms[spot]):
|
||||
return
|
||||
|
||||
cost = move_cost(board, spot, i, Direction.ROOM)
|
||||
|
||||
# Can't move there yet, there's an obstacle in the way
|
||||
if cost is None:
|
||||
return
|
||||
|
||||
# Update the board state
|
||||
alley, rooms = board
|
||||
rooms = rooms[:spot] + ((spot,) + rooms[spot],) + rooms[spot + 1 :]
|
||||
alley = alley[:i] + (None,) + alley[i + 1 :]
|
||||
|
||||
yield Move(cost, Board(alley, rooms))
|
||||
|
||||
def rooms_moves_for(board: Board, i: int) -> Iterator[Move]:
|
||||
room = board.rooms[i]
|
||||
# No need to move out of a solved room
|
||||
if all(a == i for a in room):
|
||||
return
|
||||
|
||||
for dest in range(len(board.alley)):
|
||||
cost = move_cost(board, i, dest, Direction.ALLEY)
|
||||
|
||||
# Can't move there yet, there's an obstacle in the way
|
||||
if cost is None:
|
||||
continue
|
||||
|
||||
# Update the board state
|
||||
alley, rooms = board
|
||||
rooms = rooms[:i] + (room[1:],) + rooms[i + 1 :]
|
||||
alley = alley[:dest] + (room[0],) + alley[dest + 1 :]
|
||||
|
||||
yield Move(cost, Board(alley, rooms))
|
||||
|
||||
def moves(board: Board) -> Iterator[Move]:
|
||||
for i in range(len(board.alley)):
|
||||
yield from alley_moves_for(board, i)
|
||||
for i in range(len(board.rooms)):
|
||||
yield from rooms_moves_for(board, i)
|
||||
|
||||
@functools.cache
|
||||
def total_cost(board: Board) -> Optional[int]:
|
||||
if board_is_solved(board):
|
||||
return 0
|
||||
|
||||
best = None
|
||||
|
||||
for cost, new_board in moves(board):
|
||||
if (end_cost := total_cost(new_board)) is None:
|
||||
continue
|
||||
cost += end_cost
|
||||
if best is None or cost < best:
|
||||
best = cost
|
||||
|
||||
return best
|
||||
|
||||
board = parse()
|
||||
cost = total_cost(board)
|
||||
assert cost is not None # Sanity check
|
||||
|
||||
return cost
|
||||
|
||||
|
||||
def main() -> None:
|
||||
input = [line.rstrip("\n") for line in sys.stdin.readlines()]
|
||||
print(solve(input))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
Loading…
Reference in a new issue