#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[ ] @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[ (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[ (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[ (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[ (module+ test (define input-str (file->string "day19-input.txt")) (check-equal? (q1 input-str) 576) (check-equal? (q2 input-str) 207))]