diff --git a/day19.py b/day19.py index a2d250b..4a4929c 100644 --- a/day19.py +++ b/day19.py @@ -1,16 +1,15 @@ from __future__ import annotations -from collections import defaultdict, deque +from collections import defaultdict from tools.aoc import AOCDay -from typing import Any - from tools.math import mul +from typing import Any class Part: def __init__(self, part_str: str): self.attr = defaultdict(int) - for desc in part_str[1:-1].split(","): + for desc in part_str[1:-1].split(","): attr, value = desc.split("=") self.attr[attr] = int(value) @@ -36,9 +35,9 @@ class WorkflowItem: return self.check_target else: match self.check_comp: - case '<': + case "<": result = part.attr[self.check_attr] < self.check_value - case '>': + case ">": result = part.attr[self.check_attr] > self.check_value case _: assert False, "unhandled comparator: %s" % self.check_comp @@ -46,11 +45,14 @@ class WorkflowItem: 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) + def reverse(self) -> WorkflowItem: + if self.check_attr is None: + return self + else: + if self.check_comp == ">": + return WorkflowItem(f"{self.check_attr}<{self.check_value + 1}:{self.check_target}") + else: + return WorkflowItem(f"{self.check_attr}>{self.check_value - 1}:{self.check_target}") class Workflow: @@ -69,118 +71,50 @@ class Workflow: 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) + def reached_workflow(self, target: str) -> list[list[WorkflowItem]]: + filter_lists = [] + my_list = [] + for item in self.flow: + if item.check_target != target: + my_list.append(item.reverse()) + else: + if item.check_attr is None: + filter_lists.append(my_list) 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) + filter_lists.append(my_list + [item]) + my_list.append(item.reverse()) - if not found_target: - return 0 + if self.source is not None: + new_filter_lists = [] + sub_lists = self.source.reached_workflow(self.name) + for sub_list in sub_lists: + for my_list in filter_lists: + new_filter_lists.append(sub_list + my_list) - #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) + filter_lists = new_filter_lists - f = mul([v['<'] - v['>'] - 1 for v in attr_dict.values()]) - #print("final", workflow.name, attr_dict, "=>", f) - return f + return filter_lists def get_unfiltered_parts(filters: list[WorkflowItem]) -> int: attr_dict = { - 'x': {'>': 0, '<': 4001}, - 'm': {'>': 0, '<': 4001}, - 'a': {'>': 0, '<': 4001}, - 's': {'>': 0, '<': 4001}, + "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 ">": + 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()]) + 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 = [ [ @@ -189,8 +123,8 @@ class Day(AOCDay): ], [ (167409079868000, "input19_test"), - (None, "input19"), - ] + (132392981697081, "input19"), + ], ] def parse_input(self) -> (dict[str, Workflow], list[Part]): @@ -213,24 +147,24 @@ class Day(AOCDay): ans = 0 for part in parts: target = workflows["in"].get_target(part) - while target not in ['A', 'R']: + while target not in ["A", "R"]: target = workflows[target].get_target(part) - if target == 'A': + 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') + if workflow.has_target("A"): + filters = workflow.reached_workflow("A") + ans += sum(get_unfiltered_parts(x) for x in filters) return ans -if __name__ == '__main__': +if __name__ == "__main__": day = Day(2023, 19) day.run(verbose=True)