Advent of Code 2023 - Day 4: Scratchcards

:: programming, puzzle, racket

1
2
#lang iracket/lang #:require racket
(require "../advent.rkt")

I always enjoy recursive list processing in Racket :) Here’s our input today:

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

For each card, the numbers before the | are “winning” numbers. The numbers after the | are “our” numbers. Our goal is to obtain a list where each element is the number indicating how many of “our” numbers matched a “winning” number. First let’s get the raw input lines after splitting on " | ":

1
2
3
4
(define lines (parse-aoc 4 (λ (s) (string-split s " | ")) #:print-sample #f))

(for ([ line lines])
    (println line))
'("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")

To reduce those lines to a single number indicating the number of matches, we’ll:

  1. Split each of the two strings on white space
  2. For the first string, drop the 1st two strings, since we don’t need them
  3. Compute the set intersection of the two resulting lists
  4. Count the number of elements in the set intersection
1
2
3
4
5
6
(define cards (map (λ (l)
                     (set-count (set-intersect (drop (string-split (car l)) 2)
                                               (string-split (cadr l)))))
                   lines))

cards

’(4 2 2 1 0 0)

Now we can solve the parts. For Part 1:

  1. Filter the cards to only the ones with positive values
  2. For each of those, compute the score with 2 ^ (n-1)
  3. Sum all of the scores
1
2
3
4
(define (part1 cards)
  (list-sum (map (λ (n) (expt 2 (sub1 n))) (filter positive? cards))))

(part1 cards)

13

Part 2 is a little more difficult, but lends itself to a straightforward recursive solution. We’ll create a function that excepts n (the number of elements to process) and cards (a list of scores). The function simply sums two numbers - n and the sum of recursively calling itself for each of the sublists.

1
2
3
4
5
6
7
(define (part2 n cards)
  (+ n (let loop ([ n n ][ cards cards ][ total 0 ])
         (cond [ (= n 0) total ]
               [ else (let ([ sub-total (part2 (car cards) (cdr cards)) ])
                        (loop (sub1 n) (cdr cards) (+ total sub-total))) ]))))

(part2 (length cards) cards)

30

Concluding Thoughts

There are a couple of things I like about the part 2 solution.

First: the solution is functional i.e. there is no mutation involved. I’ve found that an immutable, functional solution is almost always easier to both create and reason about.

Second: it’s very efficient because once we create the initial list of cards, we’re not allocating any additional data structures - we’re simply traversing the same list repeatedly with calls to cdr, which returns the tail of the list via a pointer. The part2 function takes about 20 ms on my computer.