diff --git a/challenges/2023/19-aplenty/README.md b/challenges/2023/19-aplenty/README.md new file mode 100644 index 0000000..5ea58a1 --- /dev/null +++ b/challenges/2023/19-aplenty/README.md @@ -0,0 +1,3 @@ +# [Day 19: Aplenty](https://adventofcode.com/2023/day/19) + +This is not a problem I expected to be able to first-try part 2 for. \ No newline at end of file diff --git a/challenges/2023/19-aplenty/main.py b/challenges/2023/19-aplenty/main.py new file mode 100644 index 0000000..96ace69 --- /dev/null +++ b/challenges/2023/19-aplenty/main.py @@ -0,0 +1,162 @@ +import sys +from collections import namedtuple +from functools import reduce + + +ConditionalRule = namedtuple("ConditionalRule", ["field", "op", "value", "next"]) +UnconditionalRule = namedtuple("UnconditionalRule", ["next"]) +Rule = ConditionalRule | UnconditionalRule +Workflows = dict[str, list[Rule]] + +Part = namedtuple("Part", ["x", "m", "a", "s"]) + + +def parse(instr: str) -> tuple[Workflows, list[Part]]: + raw_workflows, raw_parts = instr.split("\n\n") + + workflows: Workflows = {} + + for line in raw_workflows.splitlines(): + bracket_start = line.find("{") + workflow_name = line[0:bracket_start] + rules = [] + + for raw_rule in line[bracket_start + 1 : -1].split(","): + colon_pos = raw_rule.find(":") + + if colon_pos == -1: + rules.append(UnconditionalRule(raw_rule)) + continue + + field = raw_rule[0] + op = raw_rule[1] + value = int(raw_rule[2:colon_pos]) + next_workflow = raw_rule[colon_pos + 1 :] + + rules.append(ConditionalRule(field, op, value, next_workflow)) + + workflows[workflow_name] = rules + + parts: list[Part] = [] + for line in raw_parts.splitlines(): + line = line[1:-1] + sp = [x.split("=") for x in line.split(",")] + assert "".join(map(lambda x: x[0], sp)) == "xmas" + parts.append(Part(*map(lambda x: int(x[1]), sp))) + + return workflows, parts + + +def test_rule(r: ConditionalRule, p: Part) -> bool: + test_value = p.__getattribute__(r.field) + match r.op: + case ">": + return test_value > r.value + case "<": + return test_value < r.value + case _: + raise ValueError(f"unknown operation {r.op}") + + +def is_acceptable(w: Workflows, p: Part) -> bool: + cursor = "in" + while not (cursor == "R" or cursor == "A"): + for rule in w[cursor]: + if (type(rule) == ConditionalRule and test_rule(rule, p)) or type( + rule + ) == UnconditionalRule: + cursor = rule.next + break + return cursor == "A" + + +def one(instr: str): + workflows, parts = parse(instr) + + acc = 0 + for part in parts: + if is_acceptable(workflows, part): + acc += sum(part) + return acc + + +Range = tuple[int, int] + + +def split_range(rng: Range, rule: ConditionalRule) -> tuple[Range | None, Range | None]: + # First range is the matching one, second range is the non-matching one. + (lower, upper) = rng + match rule.op: + case "<": + if upper < rule.value: + return rng, None + if lower >= rule.value: + return None, rng + return ((lower, rule.value - 1), (rule.value, upper)) + case ">": + if lower > rule.value: + return rng, None + if upper <= rule.value: + return None, rng + return ((rule.value + 1, upper), (lower, rule.value)) + case _: + raise ValueError(f"unknown operation {rule.op}") + + +def get_acceptable_ranges( + workflows: Workflows, workflow_name: str, ranges: dict[str, Range] +) -> list[dict[str, Range]]: + if workflow_name == "A": + return [ranges] + if workflow_name == "R": + return [] + + res = [] + + for rule in workflows[workflow_name]: + if type(rule) == UnconditionalRule: + res += get_acceptable_ranges(workflows, rule.next, ranges) + continue + + matches, not_matches = split_range(ranges[rule.field], rule) + + if matches is not None: + x = ranges.copy() + x[rule.field] = matches + res += get_acceptable_ranges(workflows, rule.next, x) + + if not_matches is not None: + ranges[rule.field] = not_matches + + return res + + +def get_range_len(r: Range) -> int: + (start, end) = r + return (end - start) + 1 + + +def two(instr: str): + workflows, _ = parse(instr) + acc = 0 + for ranges in get_acceptable_ranges( + workflows, "in", {c: [1, 4000] for c in "xmas"} + ): + acc += reduce(lambda acc, x: acc * get_range_len(x), ranges.values(), 1) + return acc + + +def _debug(*args, **kwargs): + kwargs["file"] = sys.stderr + print(*args, **kwargs) + + +if __name__ == "__main__": + if len(sys.argv) < 2 or sys.argv[1] not in ["1", "2"]: + print("Missing day argument", file=sys.stderr) + sys.exit(1) + inp = sys.stdin.read().strip() + if sys.argv[1] == "1": + print(one(inp)) + else: + print(two(inp)) diff --git a/challenges/2023/19-aplenty/tests.json b/challenges/2023/19-aplenty/tests.json new file mode 100644 index 0000000..bbad668 --- /dev/null +++ b/challenges/2023/19-aplenty/tests.json @@ -0,0 +1,14 @@ +{ + "1": [ + { + "is": "19114", + "input": "px{a<2006:qkq,m>2090:A,rfg}\npv{a>1716:R,A}\nlnx{m>1548:A,A}\nrfg{s<537:gd,x>2440:R,A}\nqs{s>3448:A,lnx}\nqkq{x<1416:A,crn}\ncrn{x>2662:A,R}\nin{s<1351:px,qqz}\nqqz{s>2770:qs,m<1801:hdj,R}\ngd{a>3333:R,R}\nhdj{m>838:A,pv}\n\n{x=787,m=2655,a=1222,s=2876}\n{x=1679,m=44,a=2067,s=496}\n{x=2036,m=264,a=79,s=2244}\n{x=2461,m=1339,a=466,s=291}\n{x=2127,m=1623,a=2188,s=1013}\n\n" + } + ], + "2": [ + { + "is": "167409079868000", + "input": "px{a<2006:qkq,m>2090:A,rfg}\npv{a>1716:R,A}\nlnx{m>1548:A,A}\nrfg{s<537:gd,x>2440:R,A}\nqs{s>3448:A,lnx}\nqkq{x<1416:A,crn}\ncrn{x>2662:A,R}\nin{s<1351:px,qqz}\nqqz{s>2770:qs,m<1801:hdj,R}\ngd{a>3333:R,R}\nhdj{m>838:A,pv}\n\n{x=787,m=2655,a=1222,s=2876}\n{x=1679,m=44,a=2067,s=496}\n{x=2036,m=264,a=79,s=2244}\n{x=2461,m=1339,a=466,s=291}\n{x=2127,m=1623,a=2188,s=1013}\n\n" + } + ] +} \ No newline at end of file diff --git a/challenges/2023/README.md b/challenges/2023/README.md index a048a88..1204eb4 100644 --- a/challenges/2023/README.md +++ b/challenges/2023/README.md @@ -31,4 +31,5 @@ A day denoted with a star means it has a visualisation. | 15 - Lens Library | ★ ★ | Go | Still took some brainpower but this time the brainpower was needed to work out what the problem was, *not* to work out how to solve the problem. | | 16 - The Floor Will Be Lava | ★ ★ | Python | Pathfinding, sort of, but also brute forceable?? | | 17 - Clumsy Crucible | ★ ★ | Python | This taught me quite a lot about how to meddle with Djikstra's | -| 18 - Ladaduct Lagoon | ★ ★ | Python | Nothing quite like a problem that I thought I knew the solution to showing up my lack of mathematical knowledge. | \ No newline at end of file +| 18 - Ladaduct Lagoon | ★ ★ | Python | Nothing quite like a problem that I thought I knew the solution to showing up my lack of mathematical knowledge. | +| 19 - Aplenty | ★ ★ | Python | So maybe I *can* do range maths? | \ No newline at end of file diff --git a/challenges/2023/benchmark-graph.png b/challenges/2023/benchmark-graph.png index b759df9..0a71b7d 100644 Binary files a/challenges/2023/benchmark-graph.png and b/challenges/2023/benchmark-graph.png differ diff --git a/challenges/2023/benchmarks.jsonl b/challenges/2023/benchmarks.jsonl index 9d425ec..29de885 100644 --- a/challenges/2023/benchmarks.jsonl +++ b/challenges/2023/benchmarks.jsonl @@ -33,3 +33,7 @@ {"day": 17, "part": 2, "runner": "py", "min": 26.201914072036743, "max": 26.201914072036743, "avg": 26.201914072036743, "n": 1} {"day": 18, "part": 1, "runner": "py", "min": 0.02330160140991211, "max": 0.03203868865966797, "avg": 0.024628419876098633, "n": 100} {"day": 18, "part": 2, "runner": "py", "min": 0.023529052734375, "max": 0.030207157135009766, "avg": 0.02483478546142578, "n": 100} +{"day": 19, "part": 1, "runner": "py", "min": 0.023938894271850586, "max": 0.05737614631652832, "avg": 0.027661228179931642, "n": 100} +{"day": 19, "part": 2, "runner": "py", "min": 0.026041030883789062, "max": 0.03458356857299805, "avg": 0.028042428493499756, "n": 100} +{"day": 19, "part": 1, "runner": "py", "min": 0.023969173431396484, "max": 0.03136777877807617, "avg": 0.026349050998687742, "n": 100} +{"day": 19, "part": 2, "runner": "py", "min": 0.024805068969726562, "max": 0.03318047523498535, "avg": 0.027637341022491456, "n": 100}