aoc2016/day11.py

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)