2023 AoC Day 19 – Aplenty

This is a solution to Advent of Code 2023 day 19, written in Raku.

https://adventofcode.com/2023/day/19

Part One

Sort through all of the parts you've been given; what do you get if you add together all of the rating numbers for all of the parts that ultimately get accepted?

class Rule { has $.category; has $.cond; has $.val; has $.action; }
class Workflow { has $.name; has @.rules; }

my $input = slurp '19-input.txt';
my ($workflows, $parts) = $input.split("\n\n")>>.lines;

my %workflows = @$workflows.map(
    -> $w {
        my $m = $w.match(
            /^ $<name>=(\w+) '{'
               $<rules>=( [$<cat>=(\w) $<cond>=(<[<>]>) $<val>=(\d+) ':' $<action>=(\w+)]
                           | $<action>=(\w+) )+ % ','
               '}' $/);
        my @rules = $m<rules>.map(
            -> $r {
                if $r<cat> {
                    my $category = $r<cat>.Str;
                    my $cond = $r<cond>.Str;
                    my $val = $r<val>.Int;
                    my $action = $r<action>.Str;
                    Rule.new(:$category, :$cond, :$val, :$action)
                } else {
                    my $action = $r<action>.Str;
                    Rule.new(:$action)
                }
            });
        my $name = $m<name>.Str;
        $name => Workflow.new(:$name, :@rules);
    });

my $total = 0;

part:
for @$parts -> $p {
    my ($x, $m, $a, $s) = $p.comb(/\d+/);
    my %vals = x => $x, m => $m, a => $a, s => $s;

    my $w = %workflows<in>;
    my $i = $w.rules.iterator;
    while $i {
        my $r = $i.pull-one;
        if $r.cond {
            if $r.cond eq '<' {
                next if not %vals{$r.category} < $r.val;
            } else {
                next if not %vals{$r.category} > $r.val;
            }
        }

        next part if $r.action eq 'R';
        if $r.action eq 'A' {
            $total += $x + $m + $a + $s;
            next part;
        }
        if %workflows{$r.action}:exists {
            $w = %workflows{$r.action};
            $i = $w.rules.iterator;
        }
    }
}

say $total;
say "Took " ~ (now - ENTER now).base(10,2) ~ " seconds";
397134
Took 0.10 seconds

Part Two

Each of the four ratings (x, m, a, s) can have an integer value ranging from a minimum of 1 to a maximum of 4000. Of all possible distinct combinations of ratings, your job is to figure out which ones will be accepted …

Consider only your list of workflows; the list of part ratings that the Elves wanted you to sort is no longer relevant. How many distinct combinations of ratings will be accepted by the Elves' workflows?

Strategy:

  • Reuse the rule parsing from part 1 and ignore the parts
  • Apply the workflow rules to a set of 1..4000 input ranges
  • Recursively sub-divide the ranges through the rule processing
  • Retain the sub-ranges that reach an A accept rule
class Rule { has $.cat; has $.cond; has $.val; has $.action; }
class Workflow { has $.name; has @.rules; }

my $input = slurp '19-input.txt';
my ($workflows, $parts) = $input.split("\n\n")>>.lines;

my %workflows = @$workflows.map(
    -> $w {
        my $m = $w.match(
            /^ $<name>=(\w+) '{'
               $<rules>=( [$<cat>=(\w) $<cond>=(<[<>]>) $<val>=(\d+) ':' $<action>=(\w+)]
                           | $<action>=(\w+) )+ % ','
               '}' $/);
        my @rules = $m<rules>.map(
            -> $r {
                if $r<cat> {
                    my $cat = $r<cat>.Str;
                    my $cond = $r<cond>.Str;
                    my $val = $r<val>.Int;
                    my $action = $r<action>.Str;
                    Rule.new(:$cat, :$cond, :$val, :$action)
                } else {
                    my $action = $r<action>.Str;
                    Rule.new(:$action)
                }
            });
        my $name = $m<name>.Str;
        $name => Workflow.new(:$name, :@rules);
    });

my @ranges;
sub evaluate-range(%r, $name) {
    return if $name eq 'R';

    if $name eq 'A' {
        @ranges.push(%r);
        return;
    }

    my $w = %workflows{$name};
    for $w.rules -> $rule {
        if ! $rule.cond {
            evaluate-range(%r, $rule.action);
        } elsif $rule.cond eq '<' {
            my %sub-r = %r;
            my $new-max = %r{$rule.cat}.max min ($rule.val - 1);
            %sub-r{$rule.cat} = %r{$rule.cat}.min .. $new-max;

            evaluate-range(%sub-r, $rule.action);

            my $new-min = %r{$rule.cat}.min max ($rule.val);
            %r{$rule.cat} = $new-min .. %r{$rule.cat}.max;
        } elsif $rule.cond eq '>' {
            my %sub-r = %r;
            my $new-min = %r{$rule.cat}.min max ($rule.val + 1);
            %sub-r{$rule.cat} = $new-min .. %r{$rule.cat}.max;

            evaluate-range(%sub-r, $rule.action);

            my $new-max = %r{$rule.cat}.max min ($rule.val);
            %r{$rule.cat} = %r{$rule.cat}.min .. $new-max;
        }
    }
}

my %r = x => 1..4000, m => 1..4000, a => 1..4000, s => 1..4000;
evaluate-range(%r, 'in');

say [+] @ranges.map(
    -> %r {
        [*] %r.values.map(-> $val { $val.max - $val.min + 1})
    });
say "Took " ~ (now - ENTER now).base(10,2) ~ " seconds";
127517902575337
Took 0.10 seconds
raku