aoc2023/day19.py

237 lines
8.0 KiB
Python

from __future__ import annotations
from collections import defaultdict, deque
from tools.aoc import AOCDay
from typing import Any
from tools.math import mul
class Part:
def __init__(self, part_str: str):
self.attr = defaultdict(int)
for desc in part_str[1:-1].split(","):
attr, value = desc.split("=")
self.attr[attr] = int(value)
def get_value(self) -> int:
return sum(self.attr.values())
class WorkflowItem:
def __init__(self, flow_str: str):
if ":" in flow_str:
check, self.check_target = flow_str.split(":")
self.check_attr = check[0]
self.check_comp = check[1]
self.check_value = int(check[2:])
else:
self.check_target = flow_str
self.check_comp = None
self.check_attr = None
self.check_value = None
def get_target(self, part: Part) -> str | None:
if self.check_attr is None:
return self.check_target
else:
match self.check_comp:
case '<':
result = part.attr[self.check_attr] < self.check_value
case '>':
result = part.attr[self.check_attr] > self.check_value
case _:
assert False, "unhandled comparator: %s" % self.check_comp
if result:
return self.check_target
def __str__(self):
return f"{self.check_attr} {self.check_comp} {self.check_value} ? => {self.check_target}"
def __repr__(self):
return str(self)
class Workflow:
def __init__(self, workflow_str: str):
workflow_str = workflow_str[:-1]
self.name, workflow = workflow_str.split("{")
self.flow = [WorkflowItem(x) for x in workflow.split(",")]
self.source = None
def get_target(self, part: Part) -> str:
for flow in self.flow:
target = flow.get_target(part)
if target is not None:
return target
def has_target(self, target: str) -> bool:
return target in [x.check_target for x in self.flow]
def get_combinations(workflow: Workflow, target: str, attr_dict: dict[str, dict[str, int]] = None) -> int:
if workflow is None:
return 0
if attr_dict is None:
attr_dict = {
'x': {'>': 0, '<': 4001},
'm': {'>': 0, '<': 4001},
'a': {'>': 0, '<': 4001},
's': {'>': 0, '<': 4001},
}
#print(f"check flow {workflow.name}")
found_target = False
for item in reversed(workflow.flow):
if item.check_target != target and not found_target:
#print(f" - ignore item {item.check_target}")
continue
if item.check_target == target and not found_target:
# handle: lkl{x>3811:A,m<1766:R,x>3645:A,R}
# handle: qfk{x<2170:A,s<3790:R,m<2700:xzk,A}
print(f" - found target {item.check_target}, {item.check_attr}, {item.check_comp}, {item.check_value}")
if item.check_attr is not None:
if item.check_comp == '>':
attr_dict[item.check_attr]['>'] = max(attr_dict[item.check_attr]['>'], item.check_value)
else:
attr_dict[item.check_attr]['<'] = min(attr_dict[item.check_attr]['<'], item.check_value)
found_target = True
else:
#print(f" - found must fail condition {item.check_target}, {item.check_attr}, {item.check_comp}, {item.check_value}")
if item.check_attr is not None and item.check_target != target:
if item.check_comp == '>':
attr_dict[item.check_attr]['<'] = min(attr_dict[item.check_attr]['<'], item.check_value + 1)
else:
attr_dict[item.check_attr]['>'] = max(attr_dict[item.check_attr]['>'], item.check_value - 1)
if not found_target:
return 0
#print(f"{workflow.name}, {attr_dict=}")
if workflow.name != 'in' and workflow.source is not None:
#print(f"recurse to {workflow.source.name}")
return get_combinations(workflow.source, workflow.name, attr_dict)
f = mul([v['<'] - v['>'] - 1 for v in attr_dict.values()])
#print("final", workflow.name, attr_dict, "=>", f)
return f
def get_unfiltered_parts(filters: list[WorkflowItem]) -> int:
attr_dict = {
'x': {'>': 0, '<': 4001},
'm': {'>': 0, '<': 4001},
'a': {'>': 0, '<': 4001},
's': {'>': 0, '<': 4001},
}
for item in filters:
match item.check_comp:
case '>':
attr_dict[item.check_attr]['>'] = max(attr_dict[item.check_attr]['>'], item.check_value)
case '<':
attr_dict[item.check_attr]['<'] = min(attr_dict[item.check_attr]['<'], item.check_value)
case _:
assert False, f"unexpected check comp {item.check_comp}, {item.check_target}"
return mul([v['<'] - v['>'] - 1 for v in attr_dict.values()])
def get_foo(workflows: dict[str, Workflow]) -> int:
filters = defaultdict(list)
a_count, r_count = 0, 0
q = deque()
q.append('in')
while q:
workflow_name = q.popleft()
print("check workflow", workflow_name)
workflow = workflows[workflow_name]
for item in workflow.flow:
print(f" - check item {item} with filters {filters[workflow_name]}")
reached_me = get_unfiltered_parts(filters[workflow_name])
print(" - before item, got reached by ", reached_me)
if item.check_attr is None:
if item.check_target == 'A':
a_count += reached_me
elif item.check_target == 'R':
r_count += reached_me
else:
filters[item.check_target].extend(filters[workflow_name])
q.append(item.check_target)
break
else:
filters[workflow_name].append(item)
survived_filter = get_unfiltered_parts(filters[workflow_name])
filter_diff = reached_me - survived_filter
print(" - after item, got reached by ", filter_diff)
if item.check_target == 'A':
a_count += filter_diff
elif item.check_target == 'R':
r_count += filter_diff
else:
if item.check_comp is not None:
filters[item.check_target].append(item)
q.append(item.check_target)
assert a_count + r_count == 4000**4, f"{a_count=} and {r_count=}"
return a_count
# 361661634528000
# 167409079868000
# 256000000000000
class Day(AOCDay):
inputs = [
[
(19114, "input19_test"),
(397643, "input19"),
],
[
(167409079868000, "input19_test"),
(None, "input19"),
]
]
def parse_input(self) -> (dict[str, Workflow], list[Part]):
workflow_list, parts = self.getMultiLineInputAsArray()
workflow_list = [Workflow(x) for x in workflow_list]
parts = [Part(x) for x in parts]
workflows = {}
for w in workflow_list:
workflows[w.name] = w
ps = [x for x in workflow_list if x.has_target(w.name)]
if ps:
assert len(ps) == 1
w.source = ps[0]
return workflows, parts
def part1(self) -> Any:
workflows, parts = self.parse_input()
ans = 0
for part in parts:
target = workflows["in"].get_target(part)
while target not in ['A', 'R']:
target = workflows[target].get_target(part)
if target == 'A':
ans += part.get_value()
return ans
def part2(self) -> Any:
workflows, _ = self.parse_input()
return get_foo(workflows)
ans = 0
for workflow in workflows.values():
if workflow.has_target('A'):
ans += get_combinations(workflow, 'A')
return ans
if __name__ == '__main__':
day = Day(2023, 19)
day.run(verbose=True)