Racket solutions & explanations for the Advent of Code puzzles
You can not 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/day19.rkt

95 lines
5.4 KiB

#lang scribble/lp2
@(require scribble/manual aoc-racket/helper)
@aoc-title[19]
@defmodule[aoc-racket/day19]
@link["http://adventofcode.com/day/19"]{The puzzle}. Our @link-rp["day19-input.txt"]{input} is list of ``molecule'' transformations that look like @tt{B => TiRnFAr}, and then a longer test molecule.
@chunk[<day19>
<day19-setup>
<day19-q1>
<day19-q2>
<day19-test>]
@isection{How many distinct molecules can be created after one transformation?}
Starting with our test molecule, we are asked to try every molecule transformation at every possible position, and count up the distinct molecules that are created.
Each molecule transformation defines a string replacement. We'll parse our input into a test molecule, and a list of transformations (= each transformation is a list of a before and after string). Because we want to perform each transformation at every possible point in the test molecule, we can't use @racket[regexp-replace] (because it only replaces the first match) or @iracket[regexp-replace*] (because it replaces all matches). Instead we'll use @iracket[regexp-match-positions*] to generate a list of match positions, and then perform the substitutions at each location to generate our list of molecules. After doing this for every transformation, we can @iracket[remove-duplicates] and see how many are left.
@chunk[<day19-setup>
(require racket rackunit)
(provide (all-defined-out))
(define (parse-input-str input-str)
(match-define (cons molecule transformation-strings)
(reverse (string-split input-str "\n")))
(define transformations
(filter-not empty?
(map (curryr string-split " => ") transformation-strings)))
(values molecule transformations))
]
@chunk[<day19-q1>
(define (transform-molecule* molecule target-atom replacement-atom)
(for/list ([pos (in-list (regexp-match-positions* (regexp target-atom) molecule))])
(match-define (cons start finish) pos)
(string-append (substring molecule 0 start)
replacement-atom
(substring molecule finish (string-length molecule)))))
(define (q1 input-str)
(define-values (molecule transformations) (parse-input-str input-str))
(length
(remove-duplicates
(append-map (λ (target replacement)
(transform-molecule* molecule target replacement))
(map first transformations) (map second transformations)))))]
@isection{What's the fewest number of transformations that will generate the test module?}
Unlike some of the puzzles, this second part is a lot harder. The idea is that the test molecule was made from a series of transformations, starting with the single atom @racket{e}. So not only do we have to reverse-engineer this process, but we have to find the most efficient path.
There were three questions in Advent of Code that I couldn't figure out. This was the first. The answer to the first question seems to provide a clue: we could reverse the transformations, perform the same replacement process, and we'd have a list of precursor molecules that could've existed one step before the test molecule. Then you repeat this process with all the possible precursors, until you get back to @racket{e}. This will work in theory, but not in practice, because it requires checking too many possibilities.
So I cheated. I adapted a @link["https://github.com/ChrisPenner/Advent-Of-Code-Polyglot/blob/master/python/19/part2.py"]{Python algorithm} for Racket. I can tell you how it works, though I'm still figuring out @italic{why} it does. This function loops through the transformations and applies them everywhere they will fit. When it reaches a dead end  meaning, the molecule hasn't changed during the loop  it randomly reorders the transformations with @iracket[shuffle] and tries again. It is strange to me that this process would converge to an answer at all, let alone the best answer, let alone so quickly.
@chunk[<day19-q2>
(define (q2 input-str)
(define-values (starting-molecule xforms) (parse-input-str input-str))
(let loop ([current-mol starting-molecule][transform-count 0]
[shuffles 0][xforms xforms])
(cond
[(equal? current-mol "e") transform-count]
[else
(define-values (xformed-mol last-count)
(for/fold ([mol current-mol][count-so-far transform-count])
([(from to) (in-parallel (map first xforms) (map second xforms))])
(values (string-replace mol to from)
(+ count-so-far (length (regexp-match* to mol))))))
(if (not (equal? current-mol xformed-mol))
(loop xformed-mol last-count shuffles xforms)
(loop starting-molecule 0 (add1 shuffles) (shuffle xforms)))])))
]
@section{Testing Day 19}
@chunk[<day19-test>
(module+ test
(define input-str (file->string "day19-input.txt"))
(check-equal? (q1 input-str) 576)
(check-equal? (q2 input-str) 207))]