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/day19.rkt

96 lines
5.4 KiB
Racket

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

#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))]