DEV Community

Cover image for Scratchcards
Robert Mion
Robert Mion

Posted on

Scratchcards

Advent of Code 2023 Day 4

Part 1

Seems pretty straightforward

  1. Create two lists of numbers
  2. Use regex to extract the numbers
  3. Find their intersections, if any
  4. Calculate 1 to the power of the number of intersections

Ready...set...go!

Create two lists of numbers

I'll do two splits:

  • split(':') to separate the lists of numbers from the game id
  • split('|') to separate each list
Use regex to extract the numbers

I'll use this simple regular expression:
/\d+/g to match each number in the separate lists

Find their intersections, if any

Here's what I want to do:

For each item in the list of winning numbers
  Check if there's a matching number in the list of numbers had
    If it's a match, keep the number
Enter fullscreen mode Exit fullscreen mode

That sounds like a combination of filter() and includes().

Thankfully, it really is that simple care of this Stack Overflow answer:

const intersection = array1.filter(
  value => array2.includes(value)
);
Enter fullscreen mode Exit fullscreen mode
Power up and sum

Lastly, I just multiply 1 as many times as the length of the intersected array:

return 1 ** intersection.length
Enter fullscreen mode Exit fullscreen mode
Ooops! I misunderstood that last step!

The instructions say to double the points each time, not multiply 1 some N number of times.

I started with this simple reduce():

intersections.reduce(total => total * 2, 1)
Enter fullscreen mode Exit fullscreen mode
  • Start at 1
  • Double as many times as there are items in the array

Unfortunately, this starts with 1 point before the first item is touched. By the time it touches the second item, the points are already 2!

To rectify this, I halved the accumulated value:

intersections.reduce(total => total * 2, 1/2)
Enter fullscreen mode Exit fullscreen mode

Unfortunately, this attributes 0.5 points to arrays with a single item, instead of 1.

To account for that, I add a short ternary operation in the return statement of the outer reduce.

Here's the combined algorithm in JavaScript:

input.reduce((sum, line) => {
  const [, list] = line.split(':')
  let [winning, had] = list.split('|')
    .map(RA => [...RA.matchAll(/\d+/g)].map(el => +el[0]))
  const intersection = winning.filter(
    value => had.includes(value)
  );
  const points = intersection.reduce(total => total * 2, 1/2)
  return sum + (points < 1 ? 0 : points)
}, 0)
Enter fullscreen mode Exit fullscreen mode

Checking, submitting and hopefully celebrating

  • It works on the example input!
  • And it works on my puzzle input!

Woohoo!

Part 2

First impression

I get a little anxious when the Part 2 instructions are of equal length as Part 1.

That usually means the rules are about to change enough for the author to merit fully re-explaining the example input.

Ok. Time to read.

That escalated quickly

Day 4 is now Day 3 reversed: Easy Part 1, Hard Part 2.

I read it, then stepped away for a while.

All the while I thought about different ways of solving this.

I feel close to a smart way of solving that just involves counting, and not duplicating game strings.

But I have to walk through this one step at a time to ensure I fully understand how I'd write a program to solve it.

Walk with me

The example cards are:

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
Enter fullscreen mode Exit fullscreen mode

Written as card-to-winning-number-counts:

1: 4
2: 2
3: 2
4: 1
5: 0
6: 0
Enter fullscreen mode Exit fullscreen mode

Tracking instances, too:

1: {matches: 4, instances: 1}
2: {matches: 2, instances: 1}
3: {matches: 2, instances: 1}
4: {matches: 1, instances: 1}
5: {matches: 0, instances: 1}
6: {matches: 0, instances: 1}
Enter fullscreen mode Exit fullscreen mode
For each card
  For each instance
    Increase the instance counter for the next N cards by 1
      Where N is the matches counter for the current card
Enter fullscreen mode Exit fullscreen mode

So, starting with Card 1:

  • Do 1 time
  • Increase the next 4 cards' instance counters by 1

Now the cards are:

1: {matches: 4, instances: 1}
2: {matches: 2, instances: 2}
3: {matches: 2, instances: 2}
4: {matches: 1, instances: 2}
5: {matches: 0, instances: 2}
6: {matches: 0, instances: 1}
Enter fullscreen mode Exit fullscreen mode

Next, for Card 2:

  • Do 2 times
  • Increase the next 2 cards' instance counters by 1

Now the cards are:

1: {matches: 4, instances: 1}
2: {matches: 2, instances: 2}
3: {matches: 2, instances: 4}
4: {matches: 1, instances: 4}
5: {matches: 0, instances: 2}
6: {matches: 0, instances: 1}
Enter fullscreen mode Exit fullscreen mode

Next, for Card 3:

  • Do 4 times
  • Increase the next 2 cards' instance counters by 1

Now the cards are:

1: {matches: 4, instances: 1}
2: {matches: 2, instances: 2}
3: {matches: 2, instances: 4}
4: {matches: 1, instances: 8}
5: {matches: 0, instances: 6}
6: {matches: 0, instances: 1}
Enter fullscreen mode Exit fullscreen mode

Next, for Card 4:

  • Do 8 times
  • Increase the next 1 cards' instance counter by 1

Now the cards are:

1: {matches: 4, instances: 1}
2: {matches: 2, instances: 2}
3: {matches: 2, instances: 4}
4: {matches: 1, instances: 8}
5: {matches: 0, instances: 14}
6: {matches: 0, instances: 1}
Enter fullscreen mode Exit fullscreen mode

Next, for Card 5:

  • Do 14 times
  • Increase the next 0 cards' instance counters by 1

The cards haven't changed

Next, for Card 6:

  • Do 1 time
  • Increase the next 0 cards' instance counters by 1

Finally, summing up all instances gets me:

  • 30 cards

That's the right answer!

This algorithm works!

And I could optimize it by only funning the instance loop when the matches counter is greater than 0.

I think I'm ready to write this thing.

Writing the algorithm

First, I must make the cards dictionary.
The code is nearly identical to Part 1.

const cards = input.reduce((cards, line) => {
  let [id, list] = line.split(':')
  id = +id.match(/\d+/)[0]
  let [winning, had] = list.split('|')
    .map(RA => [...RA.matchAll(/\d+/g)].map(el => +el[0]))
  const intersection = winning.filter(
    value => had.includes(value)
  );
  cards[id] = {
    matches: intersection.length,
    instances: 1
  }
  return cards
}, {})
Enter fullscreen mode Exit fullscreen mode

Next, I have to iterate through each card and update the instance counters:

for (let card in cards) {
  if (cards[card].matches > 0) {
    for (let i = 0; i < cards[card].instances; i++) {
      for (let m = 1 ; m <= cards[card].matches; m++) {
        cards[+card + m].instances++
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Lastly, I have to extract and sum up all instance values:

Object.values(cards)
  .map(card => card.instances)
  .reduce((sum, current) => sum + current)
Enter fullscreen mode Exit fullscreen mode

Checking, submitting and celebrating

  • It worked on the example input
  • And it worked on my puzzle input!

Wow, that turned out to be a lot easier than I first imagined.

I'm glad I stepped away and thought carefully about only the important values that needed tracking.

What a fun Part 2 puzzle!

Bring it on, Day 5!

Top comments (0)