This commit is contained in:
Stefan Harmuth 2023-12-19 12:18:12 +01:00
parent 215b52ff63
commit c0ffc5a8df

164
day19.py
View File

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