• 0 Posts
  • 8 Comments
Joined 1 year ago
cake
Cake day: June 10th, 2023

help-circle
  • Raku

    Today I’m thankful that I have the combinations() method available. It’s not hard to implement combinations(), but it’s not particularly interesting. This code is a bit anachronistic because I solved part 1 by expanding the universe instead of contracting it, but this way makes the calculations for part 1 and part 2 symmetric. I was worried for a bit there that I’d have to do some “here are all the places where expansion happens, check this list when calculating distances” bookkeeping, and I was quite relieved when I realized that I could just use arithmetic.

    edit: Also, first time using the Slip class in Raku, which is mind bending, but very useful for expanding/contracting the universe and generating the lists of galaxy coordinates. And I learned a neat way to transpose 2D arrays using [Z].

    View the code on Github

    Code
    use v6;
    
    sub MAIN($input) {
        my $file = open $input;
    
        my @map = $file.lines».comb».Array;
        my @galaxies-original = @map».grep("#", :k).grep(*.elems > 0, :kv).rotor(2).map({($_[0] X $_[1]).Slip});
        my $distances-original = @galaxies-original.List.combinations(2).map({($_[0] Z- $_[1])».abs.sum}).sum;
    
        # contract the universe
        @map = @map.map: {$_.all eq '.' ?? slip() !! $_};
        @map = [Z] @map;
        @map = @map.map: {$_.all eq '.' ?? slip() !! $_};
        @map = [Z] @map;
    
        my @galaxies = @map».grep("#", :k).grep(*.elems > 0, :kv).rotor(2).map({($_[0] X $_[1]).Slip});
        my $distances-contracted = @galaxies.List.combinations(2).map({($_[0] Z- $_[1])».abs.sum}).sum;
    
        my $distances-twice-expanded = ($distances-original - $distances-contracted) * 2 + $distances-contracted;
        say "part 1: $distances-twice-expanded";
        my $distances-many-expanded = ($distances-original - $distances-contracted) * 1000000 + $distances-contracted;
        say "part 2: $distances-many-expanded";
    }
    

  • Raku

    My solution for today is quite sloppy. For part 2, I chose to color along both sides of the path (each side different colors) and then doing a fill of the empty space based on what color the empty space is touching. Way less optimal than scanning, and I didn’t cover every case for coloring around the start point, but it was interesting to attempt. I ran into a bunch of issues on dealing with nested arrays in Raku, I need to investigate if there’s a better way to handle them.

    View code on github

    Edit: did some cleanup, added some fun, and switched to the scanning algorithm for part 2, shaved off about 50 lines of code.

    Code
    use v6;
    
    sub MAIN($input) {
        my $file = open $input;
    
        my @map = $file.lines».comb».Array;
    
        my @starting-point = @map».grep('S', :k)».[0].grep(*.defined, :kv).List;
    
        my @path = (@starting-point,);
    
        my %tile-neighbors =
            '|' => (( 1, 0),(-1, 0)),
            '-' => (( 0,-1),( 0, 1)),
            'L' => ((-1, 0),( 0, 1)),
            'J' => ((-1, 0),( 0,-1)),
            '7' => (( 1, 0),( 0,-1)),
            'F' => (( 1, 0),( 0, 1)),
        ;
    
        sub connecting-neighbor(@position, @neighbor) {
            my @neighbor-position = @position Z+ @neighbor;
            return False if any(@neighbor-position Z< (0, 0));
            return False if any(@neighbor-position Z> (@map.end, @map.head.end));
            my $neighbor-tile = @map[@neighbor-position[0]; @neighbor-position[1]];
            my @negative-neighbor = @neighbor X* -1;
            return %tile-neighbors{$neighbor-tile}.grep(@negative-neighbor, :k).elems > 0;
        }
    
        # replace starting-point with the appropriate pipe
        my @start-tile-candidates = <| - L J 7 F>;
        for @start-tile-candidates -> $candidate {
            next if %tile-neighbors{$candidate}.map({!connecting-neighbor(@starting-point, $_)}).any;
            @map[@starting-point[0]; @starting-point[1]] = $candidate;
            last;
        }
    
        repeat {
            my @position := @path.tail;
            my $tile = @map[@position[0]; @position[1]];
            my @neighbors = %tile-neighbors{$tile}.List;
            for @neighbors -> @neighbor {
                my @neighbor-position = @neighbor Z+ @position;
                next if @path.elems >= 2 && @neighbor-position eqv @path[*-2];
                if connecting-neighbor(@position, @neighbor) {
                    @path.push(@neighbor-position);
                    last;
                }
            }
        } while @path.tail !eqv @path.head;
        my $part-one-solution = (@path.elems / 2).floor;
        say "part 1: {$part-one-solution}";
    
        my %pipe-set = @path.Set;
        my %same-side-pairs = ;
        my $part-two-solution = 0;
        for ^@map.elems -> $y {
            my $inside = False;
            my $entrance-pipe = Nil;
            for ^@map.head.elems -> $x {
                if %pipe-set{$($y, $x)} {
                    given @map[$y; $x] {
                        when '|' { $inside = !$inside }
                        when 'F' | 'L' { $entrance-pipe = $_ }
                        when 'J' | '7' {
                            $inside = !$inside if %same-side-pairs{$entrance-pipe} ne $_;
                            $entrance-pipe = Nil;
                        }
                    }
                } elsif $inside {
                    $part-two-solution += 1;
                }
            }
        }
        say "part 2: $part-two-solution";
    }
    

  • Raku

    First time using Grammar Actions Object to make parsing a little cleaner. I thought about not keeping track of the left and right values (and I originally didn’t for part 1), but I think keeping track allows for an easier to understand solution.

    View code on github

    edit: although I don’t know why @values.all != 0 evaluates to true why any value is not zero. I thought that @values.any != 0 would do that, but it seems that their behavior is flipped from my expectations.

    edit2: Oh, I think I understand now. != is a shortcut for !==, and !== is actually the equality operator that is then negated. You can negate most relational operators in Raku by prefixing them with !. So the junction is actually binding to the == equality operator and not the !== inequality operator. Therefore @values.all != 0 becomes !(@values.all == 0). I’m not sure why they would choose this order of operations, though.

    edit3: Ah, it’s in the documentation, so it’s not even an oversight. https://github.com/rakudo/rakudo/issues/3748

    Code (probably still doesn't render correctly)
    use v6;
    
    sub MAIN($input) {
        my $file = open $input;
    
        grammar Oasis {
            token TOP { +%"\n" "\n"* }
            token history { +%\h+ }
            token val { '-'? \d+ }
        }
    
        class OasisActions {
            method TOP ($/) { make $».made }
            method history ($/) { make $».made }
            method val ($/) { make $/.Int }
        }
    
        my $oasis = Oasis.parse($file.slurp, actions => OasisActions.new);
        my @histories = $oasis.made;
        my $part-one-solution;
        my $part-two-solution;
        sub revdiff { $^b - $^a }
        for @histories -> @history {
            my @values = @history;
            my @rightmosts = [@values.tail];
            my @leftmosts = [@values.head];
            while @values.all != 0 {
                @values = @values.tail(*-1) Z- @values.head(*-1);
                @rightmosts.push(@values.tail);
                @leftmosts.push(@values.head);
            }
            $part-one-solution += [+] @rightmosts;
            $part-two-solution += [[&revdiff]] @leftmosts.reverse;
        }
        say "part 1: $part-one-solution";
        say "part 2: $part-two-solution";
    }
    


  • Personally, I’m not a fan of requiring analysis of the individualized input to reach the correct (sufficiently efficient) solution for part 2. Or maybe I’m just resentful because I feel like I’ve been duped after writing an generalized-to-the-puzzle-description-but-insufficiently-efficient solution. 😔

    These quantum ghosts need to come back down to reality.


  • Raku

    My hand-type strength calculations could probably be trimmed down a bit. I didn’t hit any big issues today.

    View code on github

    Code (note: doesn't currently display correctly on Lemmy website)
    use v6;
    
    sub MAIN($input) {
        my $file = open $input;
    
        grammar CamelCards {
            token TOP { +%"\n" "\n"*}
            token row {  " "  }
            token hand { \S+ }
            token bid { \d+ }
        }
    
        my $camel-cards = CamelCards.parse($file.slurp);
        my @rows = $camel-cards.map({ (..Str, ..Int) });
        my @ranked-rows1 = @rows.sort({hand-strength($_[0], &hand-type-strength1, '23456789TJQKA'.comb)});
        my $part-one-solution = (@ranked-rows1»[1] Z* 1..*).sum;
        say "part 1: $part-one-solution";
    
        my @ranked-rows2 = @rows.sort({hand-strength($_[0], &hand-type-strength2, 'J23456789TQKA'.comb)});
        my $part-two-solution = (@ranked-rows2»[1] Z* 1..*).sum;
        say "part 2: $part-two-solution";
    }
    
    sub hand-strength($hand, &hand-type-strength, @card-strengths) {
        my $strength = &hand-type-strength($hand);
        for $hand.comb -> $card {
            $strength = $strength +< 8 + @card-strengths.first({ $_ eq $card }, :k);
        }
        return $strength;
    }
    
    sub hand-type-strength1($hand) {
        my @sorted = $hand.comb.sort;
        my @runs = [1];
        my $card = @sorted[0];
        for @sorted[1..*] -> $new-card {
            if $new-card eq $card {
                @runs.tail += 1;
            } else {
                @runs.push(1);
                $card = $new-card;
            }
        }
        return do given @runs.sort {
            when .[0] == 5 { 6 } # Five of a kind
            when .[1] == 4 { 5 } # Four of a kind
            when .[1] == 3 { 4 } # Full House
            when .[2] == 3 { 3 } # Three of a kind
            when .[1] == 2 { 2 } # Two pair
            when .[3] == 2 { 1 } # One pair
            default { 0 } # High card
        };
    }
    
    sub hand-type-strength2($hand) {
        my @sorted = $hand.comb.grep(none /J/).sort;
        if @sorted.elems == 0 {
            return 6;
        } else {
            my @runs = [1];
            my $card = @sorted[0];
            for @sorted[1..*] -> $new-card {
                if $new-card eq $card {
                    @runs.tail += 1;
                } else {
                    @runs.push(1);
                    $card = $new-card;
                }
            }
            @runs.=sort;
            @runs.tail += 5 - @sorted.elems;
            return do given @runs {
                when .[0] == 5 { 6 } # Five of a kind
                when .[1] == 4 { 5 } # Four of a kind
                when .[1] == 3 { 4 } # Full House
                when .[2] == 3 { 3 } # Three of a kind
                when .[1] == 2 { 2 } # Two pair
                when .[3] == 2 { 1 } # One pair
                default { 0 } # High card
            };
        }
    }
    


  • Raku

    I spent a lot more time than necessary optimizing the count-ways-to-beat function, but I’m happy with the result. This is my first time using the | operator to flatten a list into function arguments.

    edit: unfortunately, the lemmy web page is unable to properly display the source code in a code block. It doesn’t display text enclosed in pointy brackets <>, perhaps it looks too much like HTML. View code on github.

    Code
    use v6;
    
    sub MAIN($input) {
        my $file = open $input;
    
        grammar Records {
            token TOP {  "\n"  "\n"* }
            token times { "Time:" \s* +%\s+ }
            token distances { "Distance:" \s* +%\s+ }
            token num { \d+ }
        }
    
        my $records = Records.parse($file.slurp);
    
        my $part-one-solution = 1;
        for $records».Int Z $records».Int -> $record {
            $part-one-solution *= count-ways-to-beat(|$record);
        }
        say "part 1: $part-one-solution";
    
        my $kerned-time = $records.join.Int;
        my $kerned-distance = $records.join.Int;
        my $part-two-solution = count-ways-to-beat($kerned-time, $kerned-distance);
        say "part 2: $part-two-solution";
    }
    
    sub count-ways-to-beat($time, $record-distance) {
        # time = button + go
        # distance = go * button
        # 0 = go^2 - time * go + distance
        # go = (time +/- sqrt(time**2 - 4*distance))/2
    
        # don't think too hard:
        # if odd t then t/2 = x.5,
        #   so sqrt(t**2-4*d)/2 = 2.3 => result = 4
        #   and sqrt(t**2-4*d)/2 = 2.5 => result = 6
        #   therefore result = 2 * (sqrt(t**2-4*d)/2 + 1/2).floor
        # even t then t/2 = x.0
        #   so sqrt(t^2-4*d)/2 = 2.x => result = 4 + 1(shared) = 5
        #   therefore result = 2 * (sqrt(t^2-4*d)/2).floor + 1
        # therefore result = 2 * ((sqrt(t**2-4*d)+t%2)/2).floor + 1 - t%2
        # Note: sqrt produces a Num, so perhaps the result could be off by 1 or 2,
        #       but it solved my AoC inputs correctly 😃.
    
        my $required-distance = $record-distance + 1;
        return 2 * ((sqrt($time**2 - 4*$required-distance) + $time%2)/2).floor + 1 - $time%2;
    }