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)