Advent of Code 2023 - Day 4: Scratchcards
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:
- Split each of the two strings on white space
- For the first string, drop the 1st two strings, since we don’t need them
- Compute the set intersection of the two resulting lists
- 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:
- Filter the cards to only the ones with positive values
- For each of those, compute the score with
2 ^ (n-1)
- 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.