advent-of-code/src/holt59/aoc/2023/day19.py

141 lines
4.4 KiB
Python
Raw Normal View History

2023-12-19 06:29:46 +00:00
import logging
import operator
import os
2023-12-04 18:32:41 +00:00
import sys
2023-12-19 06:29:46 +00:00
from math import prod
from typing import Literal, TypeAlias, cast
2023-12-04 18:32:41 +00:00
2023-12-19 06:29:46 +00:00
VERBOSE = os.getenv("AOC_VERBOSE") == "True"
logging.basicConfig(level=logging.INFO if VERBOSE else logging.WARNING)
Category: TypeAlias = Literal["x", "m", "a", "s"]
Part: TypeAlias = dict[Category, int]
PartWithBounds: TypeAlias = dict[Category, tuple[int, int]]
OPERATORS = {"<": operator.lt, ">": operator.gt}
# None if there is no check (last entry), otherwise (category, sense, value)
Check: TypeAlias = tuple[Category, Literal["<", ">"], int] | None
# workflow as a list of check, in specified order, with target
Workflow: TypeAlias = list[tuple[Check, str]]
def accept(workflows: dict[str, Workflow], part: Part) -> bool:
workflow = "in"
decision: bool | None = None
while decision is None:
for check, target in workflows[workflow]:
2023-12-20 13:18:17 +00:00
passed = check is None
2023-12-19 06:29:46 +00:00
if check is not None:
category, sense, value = check
2023-12-20 13:18:17 +00:00
passed = OPERATORS[sense](part[category], value)
2023-12-19 06:29:46 +00:00
2023-12-20 13:18:17 +00:00
if passed:
2023-12-19 06:29:46 +00:00
if target in workflows:
workflow = target
else:
decision = target == "A"
break
return decision
def propagate(workflows: dict[str, Workflow], start: PartWithBounds) -> int:
2023-12-20 13:18:17 +00:00
def _fmt(meta: PartWithBounds) -> str:
return "{" + ", ".join(f"{k}={v}" for k, v in meta.items()) + "}"
2023-12-19 06:29:46 +00:00
def transfer_or_accept(
target: str, meta: PartWithBounds, queue: list[tuple[PartWithBounds, str]]
) -> int:
2023-12-20 13:18:17 +00:00
count = 0
2023-12-19 06:29:46 +00:00
if target in workflows:
2023-12-20 13:18:17 +00:00
logging.info(f" transfer to {target}")
2023-12-19 06:29:46 +00:00
queue.append((meta, target))
elif target == "A":
2023-12-20 13:18:17 +00:00
count = prod((high - low + 1) for low, high in meta.values())
logging.info(f" accepted ({count})")
2023-12-19 06:29:46 +00:00
else:
2023-12-20 13:18:17 +00:00
logging.info(" rejected")
return count
2023-12-19 06:29:46 +00:00
accepted = 0
queue: list[tuple[PartWithBounds, str]] = [(start, "in")]
2023-12-20 13:18:17 +00:00
n_iterations = 0
2023-12-19 06:29:46 +00:00
while queue:
2023-12-20 13:18:17 +00:00
n_iterations += 1
2023-12-19 06:29:46 +00:00
meta, workflow = queue.pop()
2023-12-20 13:18:17 +00:00
logging.info(f"{workflow}: {_fmt(meta)}")
2023-12-19 06:29:46 +00:00
for check, target in workflows[workflow]:
if check is None:
2023-12-20 13:18:17 +00:00
logging.info(" end-of-workflow")
2023-12-19 06:29:46 +00:00
accepted += transfer_or_accept(target, meta, queue)
continue
category, sense, value = check
bounds, op = meta[category], OPERATORS[sense]
2023-12-20 13:18:17 +00:00
logging.info(f" checking {_fmt(meta)} against {category} {sense} {value}")
2023-12-19 06:29:46 +00:00
if not op(bounds[0], value) and not op(bounds[1], value):
2023-12-20 13:18:17 +00:00
logging.info(" reject, always false")
2023-12-19 06:29:46 +00:00
continue
if op(meta[category][0], value) and op(meta[category][1], value):
2023-12-20 13:18:17 +00:00
logging.info(" accept, always true")
2023-12-19 06:29:46 +00:00
accepted += transfer_or_accept(target, meta, queue)
break
meta2 = meta.copy()
2023-12-20 13:18:17 +00:00
low, high = meta[category]
2023-12-19 06:29:46 +00:00
if sense == "<":
2023-12-20 13:18:17 +00:00
meta[category], meta2[category] = (value, high), (low, value - 1)
2023-12-19 06:29:46 +00:00
else:
2023-12-20 13:18:17 +00:00
meta[category], meta2[category] = (low, value), (value + 1, high)
logging.info(f" split {_fmt(meta2)} ({target}), {_fmt(meta)}")
2023-12-19 06:29:46 +00:00
accepted += transfer_or_accept(target, meta2, queue)
2023-12-20 13:18:17 +00:00
logging.info(f"run took {n_iterations} iterations")
2023-12-19 06:29:46 +00:00
return accepted
workflows_s, parts_s = sys.stdin.read().strip().split("\n\n")
workflows: dict[str, Workflow] = {}
for workflow_s in workflows_s.split("\n"):
name, block_s = workflow_s.split("{")
workflows[name] = []
for block in block_s[:-1].split(","):
check: Check
if (i := block.find(":")) >= 0:
2024-12-03 13:11:29 +00:00
check = (
2023-12-19 06:29:46 +00:00
cast(Category, block[0]),
cast(Literal["<", ">"], block[1]),
int(block[2:i]),
2024-12-03 13:11:29 +00:00
)
target = block[i + 1 :]
2023-12-19 06:29:46 +00:00
else:
check, target = None, block
workflows[name].append((check, target))
2023-12-04 18:32:41 +00:00
# part 1
2023-12-19 06:29:46 +00:00
parts: list[Part] = [
{cast(Category, s[0]): int(s[2:]) for s in part_s[1:-1].split(",")}
for part_s in parts_s.split("\n")
]
answer_1 = sum(sum(part.values()) for part in parts if accept(workflows, part))
2023-12-04 18:32:41 +00:00
print(f"answer 1 is {answer_1}")
2023-12-19 06:29:46 +00:00
2023-12-04 18:32:41 +00:00
# part 2
2023-12-19 06:29:46 +00:00
answer_2 = propagate(
workflows, {cast(Category, c): (1, 4000) for c in ["x", "m", "a", "s"]}
)
2023-12-04 18:32:41 +00:00
print(f"answer 2 is {answer_2}")