Advent of Code 2023 — Day 4

My favourite so far!

Inspired by some folks at work, I decided to try and do this as functionally as possible.

Apparently there are some similarities with past puzzles, but I, for the life of me, cannot remember any of the puzzles of years past 😆.

Part 1

Today's puzzle revolved around scratch-and-win cards.

A card looks like this:

Card 1: 41 48 83 86 17 | 83 86 6 31 17 9 48 53
Card 2: 13 32 20 16 61 | 61 30 68 82 17 32 24 19
Card 3: 1 21 53 59 44 | 69 82 63 72 16 21 14 1
Card 4: 41 92 73 84 69 | 59 84 76 51 58 5 54 83
Card 5: 87 83 26 28 32 | 88 30 70 12 93 22 82 36
Card 6: 31 18 13 56 72 | 74 77 10 23 35 67 36 11

On the left of the pipe are your numbers, on the right, are numbers that have been chosen as winners. Your first winning number was worth 1 point, and each subsequent win would double your score.

So 4 wins would be (1 * 2 * 2 * 2) = 8

In part 1, we need to calculate the score of our card.

input  
  .map { |l|  
    l.gsub(/Card \d+[:]\s/, "")  
      .split("|")  
      .map{|v| v.split(" ")}  
  }  
  .map { |c|  
    c[0] & c[1]  
  }  
  .map { |c|  
    next 0 if c.count <= 0  
    1 * 2**(c.count-1)  
  }  
  .inject(:+)

That was my code for part 1.

First throw away the Card N: bits, then split my numbers from the winning numbers, and finally split both sides in to their own array.

Next, map over each pair and count the matches (n), then for each card, get the score 1 * 2^(n-1) and finally sum everything up!

⭐️ Success!

Part 2

Enter, recursion~

In this part, instead of winning points for a card, we win more cards!, which in turn win MORE cards — and so on until we're out of cards.

The ask this time? How many cards did we win in total.

First up a couple of functions to make this a bit nicer:

def play(cards, index)  
  @tally += 1

  cards  
    .slice(index + 1, wins(cards[index]))  
    .each_with_index do |_, idx|  
      play(cards, index + 1 + idx)  
    end  
end  

def wins(card)  
  (card[0] & card[1]).count  
end

play loops over all the cards we've won and plays each one, which then loops over all the cards they win and plays each one, which then loops ...

wins was just a convenience function for calculating wins.

Now we can do our tallying:

@tally = 0  
cards = input  
          .map { |l|  
            l.gsub(/Card \d+[:]\s/, "")  
             .split("|")  
             .map{|v| v.split(" ")}  
          }  

cards.each_with_index do |_, index|  
  play(cards, index)  
end  

@tally

First throw away all of the Cards: info and pre-process our cards in to the same shape as before, then, play each round.

After many SystemStackError: stack level too deep errors, I got there (always remember to increment your indices folks).

⭐️ Success!

Performance

This one was a good candidate for some perf gains.

Rehearsal ------------------------------------------
part 1
  0.001061 0.000019 0.001080 ( 0.001079)
part 2
 13.300407 0.015743 13.316150 ( 13.330276)
-------------------------------- total: 13.317230sec

             user system total real
part 1
  0.000637 0.000006 0.000643 ( 0.000644)
part 2
 13.554139 0.020912 13.575051 ( 13.608991)

The code above for part 2 was suuuuper slow. Clocking in at ~13s.

Rehearsal ------------------------------------------
part 1
  0.001129 0.000038 0.001167 ( 0.001173)
part 2
  2.834447 0.007401 2.841848 ( 2.845803)
--------------------------------- total: 2.843015sec

             user system total real
part 1
  0.000636 0.000003 0.000639 ( 0.000637)
part 2 
  2.838135 0.007346 2.845481 ( 2.849414)

After adding a super simple cache to wins, it shaved ~10s off the total time:

def wins(card_id, card)  
  @win_cache[card_id] ||= (card[0] & card[1]).count
end

Happy with that!

Day 4: https://github.com/dNitza/advent-of-code/tree/main/2023/04

Published

by Daniel Nitsikopoulos

in posts

Tagged

© 2010 - 2024 Daniel Nitsikopoulos. All rights reserved.

🕸💍  →