You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
This repo is archived. You can view files and clone it, but cannot push or open issues/pull-requests.
aoc-racket/day1.scrbl

121 lines
5.5 KiB
Plaintext

9 years ago
#lang scribble/lp2
9 years ago
@(require scribble/manual aoc-racket/helper)
9 years ago
@aoc-title[1]
9 years ago
@link["http://adventofcode.com/day/1"]{The puzzle}. Our @link-rp["day1-input.txt"]{input} is a string of parentheses that controls an elevator. A left parenthesis @litchar{(} means go up one floor, and a right parenthesis @litchar{)} means go down.
9 years ago
@chunk[<day1>
9 years ago
<day1-setup>
<day1-q1>
<day1-q1-alt>
<day1-q2>
<day1-q2-alt>
<day1-test>]
9 years ago
9 years ago
@section{Where does the elevator land?}
The building has an indefinite number of floors in both directions. So the ultimate destination is just the number of up movements minus the number of down movements. In other words, a left parenthesis = @racket[1] and a right parenthesis = @racket[-1], and we sum them.
@racket[regexp-match*] will return a list of all occurrences of one string within another. The length of this list is the number of occurrences. Therefore, we can use it to count the ups and downs.
9 years ago
@chunk[<day1-setup>
9 years ago
(require racket rackunit)
(define up-char #\()
(define down-char #\))
(define (make-matcher c)
(λ(str) (length (regexp-match* (regexp (format "\\~a" c)) str))))
(define get-ups (make-matcher up-char))
(define get-downs (make-matcher down-char))
(define (get-destination str) (- (get-ups str) (get-downs str)))]
9 years ago
@chunk[<day1-q1>
9 years ago
(define (q1 str)
(displayln (format "ups = ~a" (get-ups str)))
(displayln (format "downs = ~a" (get-downs str)))
(define destination (get-destination str))
(displayln (format "destination = ~a" destination))
destination)]
9 years ago
@subsection{Alternate approach: numerical conversion}
9 years ago
9 years ago
Rather than counting matches with @racket[regexp-match*], we could also convert the string of parentheses directly into a list of numbers.
9 years ago
9 years ago
@chunk[<day1-q1-alt>
9 years ago
(define (elevator-string->ints str)
(for/list ([c (in-string str)])
(if (equal? c up-char)
1
-1)))
(define (q1-alt str)
(define destination (apply + (elevator-string->ints str)))
(displayln (format "destination = ~a" destination))
destination)]
9 years ago
@section[#:tag "q2"]{At what point does the elevator enter the basement?}
9 years ago
9 years ago
The elevator is in the basement whenever it's at a negative-valued floor. So instead of looking at its ultimate destination, we need to follow the elevator along its travels, computing its intermediate destinations, and stop as soon as it reaches a negative floor.
9 years ago
We could characterize this as a problem of tracking @italic{cumulative values} or @italic{state}. Either way, @racket[for/fold] is the weapon of choice. We'll determine the relative movement at each step, and collect these in a list. (The @racket[get-destination] function is used within the loop to convert each parenthesis into a relative movement, either @racket[1] or @racket[-1].) On each loop, @racket[for/fold] checks the cumulative value of these positions, and stops when they imply a basement value. The length of this list is our answer.
@margin-note{Nothing wrong with @racket[foldl] and @racket[foldr], but @racket[for/fold] is more flexible, and makes more readable code.}
9 years ago
@chunk[<day1-q2>
9 years ago
(define (in-basement? movements)
(negative? (apply + movements)))
(define (q2 str)
(define relative-movements
(for/fold ([movements-so-far empty])
([c (in-string str)]
#:break (in-basement? movements-so-far))
(cons (get-destination (~a c)) movements-so-far)))
(define basement-position (length relative-movements))
(displayln (format "basement entered at position = ~a" basement-position))
basement-position)]
9 years ago
@subsection{Alternate approaches: @tt{for/first} or @tt{for/or}}
9 years ago
When you need to stop a loop the first time a condition occurs, you can also consider @racket[for/first] or @racket[for/or]. The difference is that @racket[for/first] ends after the first evaluation of the body, but @racket[for/or] evaluates the body every time, and ends the first time the body is not @racket[#f].
The two are similar. The choice comes down to readability and efficiency — meaning, if each iteration of the loop is expensive, you probably will want to cache intermediate values, which means you might as well use @racket[for/fold].
9 years ago
@chunk[<day1-q2-alt>
9 years ago
(define (q2-for/first str)
(define basement-position
(let ([ints (elevator-string->ints str)])
(for/first ([idx (in-range (length ints))]
#:when (negative? (apply + (take ints idx))))
idx)))
(displayln (format "basement entered at position = ~a" basement-position))
basement-position)
(define (q2-for/or str)
(define basement-position
(let ([ints (elevator-string->ints str)])
(for/or ([idx (in-range (length ints))])
(and (negative? (apply + (take ints idx))) idx))))
(displayln (format "basement entered at position = ~a" basement-position))
basement-position)]
9 years ago
@section{Testing Day 1}
9 years ago
9 years ago
@chunk[<day1-test>
9 years ago
(module+ test
(define input-str (file->string "day1-input.txt"))
9 years ago
(check-equal? (q1 input-str) 74)
(check-equal? (q1-alt input-str) 74)
(check-equal? (q2 input-str) 1795)
(check-equal? (q2-for/first input-str) 1795)
(check-equal? (q2-for/or input-str) 1795))]