201 lines
5.6 KiB
Python
201 lines
5.6 KiB
Python
from __future__ import annotations
|
|
import itertools
|
|
from collections import deque
|
|
from enum import Enum
|
|
from tools.aoc import AOCDay
|
|
from typing import Any
|
|
|
|
|
|
class ItemType(int, Enum):
|
|
GENERATOR = 0
|
|
MICROCHIP = 1
|
|
|
|
|
|
class Item:
|
|
def __init__(self, element: str, typ: ItemType):
|
|
self.element = element
|
|
self.type = typ
|
|
|
|
def compatible(self, other: Item | Generator | Microchip) -> bool:
|
|
if self.type == other.type or self.element == other.element:
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
def __str__(self):
|
|
return "%s(%s)" % (self.__class__.__name__, self.element)
|
|
|
|
def __repr__(self):
|
|
return str(self)
|
|
|
|
|
|
class Generator(Item):
|
|
def __init__(self, element: str):
|
|
super().__init__(element, ItemType.GENERATOR)
|
|
|
|
|
|
class Microchip(Item):
|
|
def __init__(self, element: str):
|
|
super().__init__(element, ItemType.MICROCHIP)
|
|
|
|
|
|
def is_valid(group: list[Generator | Microchip]) -> bool:
|
|
generators = set()
|
|
microchips = set()
|
|
for item in group:
|
|
if item.type == ItemType.GENERATOR:
|
|
generators.add(item.element)
|
|
else:
|
|
microchips.add(item.element)
|
|
|
|
if not generators or not microchips:
|
|
return True
|
|
|
|
return all(m in generators for m in microchips)
|
|
|
|
|
|
def get_legal_moves(
|
|
from_floor: int, floors: list[list[Generator | Microchip]], debug: bool = False
|
|
) -> set[tuple[int, Microchip | Generator, ...]]:
|
|
targets = []
|
|
if 0 < from_floor:
|
|
targets.append(from_floor - 1)
|
|
if from_floor < len(floors) - 1:
|
|
targets.append(from_floor + 1)
|
|
|
|
moves = set()
|
|
if len(floors[from_floor]) == 1:
|
|
for target in targets:
|
|
if is_valid(floors[target] + [floors[from_floor][0]]):
|
|
moves.add((target, floors[from_floor][0]))
|
|
else:
|
|
for a, b in itertools.combinations(floors[from_floor], 2):
|
|
if not a.compatible(b):
|
|
continue
|
|
|
|
for target in targets:
|
|
if is_valid(floors[target] + [a]):
|
|
moves.add((target, a))
|
|
if is_valid(floors[target] + [b]):
|
|
moves.add((target, b))
|
|
if is_valid(floors[target] + [a, b]):
|
|
moves.add((target, a, b))
|
|
|
|
return moves
|
|
|
|
|
|
def move_items(
|
|
floors: list[list[Generator | Microchip]], from_floor: int, move: tuple[int, Microchip | Generator, ...]
|
|
) -> None:
|
|
to_floor = move[0]
|
|
for item in move[1:]:
|
|
floors[from_floor].remove(item)
|
|
floors[to_floor].append(item)
|
|
|
|
|
|
def floor_hash(floor: int, floors: list[list[Generator | Microchip]]) -> str:
|
|
return (
|
|
str(floor)
|
|
+ "@"
|
|
+ ";".join(
|
|
"%d:%s"
|
|
% (
|
|
i,
|
|
",".join(
|
|
x.__class__.__name__[0] + x.element[0]
|
|
for x in sorted(floors[i], key=lambda f: (f.__class__.__name__, f.element))
|
|
),
|
|
)
|
|
for i in range(len(floors))
|
|
)
|
|
)
|
|
|
|
|
|
def copy_floors(floors: list[list[Generator | Microchip]]) -> list[list[Generator | Microchip]]:
|
|
return [list(floors[x]) for x in range(len(floors))]
|
|
|
|
|
|
def all_on_floor(floors: list[list[Generator | Microchip]], target_floor: int) -> bool:
|
|
all_floors_empty = True
|
|
for i in range(len(floors)):
|
|
if i != target_floor and len(floors[i]) > 0:
|
|
all_floors_empty = False
|
|
|
|
return all_floors_empty
|
|
|
|
|
|
def get_min_steps(floors: list[list[Generator | Microchip]]) -> int:
|
|
q = deque([(0, 3, floors)])
|
|
seen = set()
|
|
while q:
|
|
dist, cur_floor_id, cur_floors = q.popleft()
|
|
if cur_floor_id == 0 and all_on_floor(cur_floors, cur_floor_id):
|
|
return dist
|
|
|
|
cur_floor_hash = floor_hash(cur_floor_id, cur_floors)
|
|
if cur_floor_hash in seen:
|
|
continue
|
|
seen.add(cur_floor_hash)
|
|
|
|
for move in get_legal_moves(cur_floor_id, cur_floors):
|
|
nxt_floors = copy_floors(cur_floors)
|
|
move_items(nxt_floors, cur_floor_id, move)
|
|
q.append((dist + 1, move[0], nxt_floors))
|
|
|
|
return 0
|
|
|
|
|
|
class Day(AOCDay):
|
|
inputs = [
|
|
[
|
|
(11, "input11_test"),
|
|
(37, "input11"),
|
|
],
|
|
[
|
|
(61, "input11"),
|
|
],
|
|
]
|
|
|
|
def parse_input(self, p2: bool = False) -> list[list[Generator | Microchip]]:
|
|
floors = []
|
|
for line in reversed(self.getInput()):
|
|
_, contents = line[:-1].split(" contains ")
|
|
if contents == "nothing relevant":
|
|
floors.append([])
|
|
continue
|
|
|
|
floor_contents = []
|
|
contents = contents.replace(", and ", ", ")
|
|
contents = contents.replace(" and ", ", ")
|
|
contents = contents.split(", ")
|
|
for content in contents:
|
|
if not content:
|
|
continue
|
|
_, element, typ = content.split()
|
|
if typ == "generator":
|
|
floor_contents.append(Generator(element))
|
|
else:
|
|
element = element.split("-")[0]
|
|
floor_contents.append(Microchip(element))
|
|
|
|
floors.append(floor_contents)
|
|
|
|
if p2:
|
|
floors[3].append(Generator("elerium"))
|
|
floors[3].append(Microchip("elerium"))
|
|
floors[3].append(Generator("dilithium"))
|
|
floors[3].append(Microchip("dilithium"))
|
|
|
|
return floors
|
|
|
|
def part1(self) -> Any:
|
|
return get_min_steps(self.parse_input())
|
|
|
|
def part2(self) -> Any:
|
|
return get_min_steps(self.parse_input(p2=True))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
day = Day(2016, 11)
|
|
day.run(verbose=True)
|