Skip to content

Latest commit

 

History

History
115 lines (93 loc) · 6.5 KB

day14.md

File metadata and controls

115 lines (93 loc) · 6.5 KB

Day Fourteen: Extended Polymerization


Preamble

Today we're going to play with string manipulation... sort of. We're given an initial polymer, and a mapping of two-character strings to the character we must insert between them. This results in the polymer growing in size very quickly, and we need to subtract the number of occurrences of the least common character from the most common character after a number of generations.

I enjoyed this problem because part 1 was very simple, but part 2 took a moment to consider because it suggested I needed a more efficient algorithm. The key point is that we don't actually care about the polymer string itself; all that matters is how many instances of each character pair appears within the list.

For instance, given the rules {"AB" "C", "BA" "D", "AA" "X"}, the input polymer "AABA" becomes "AXACBDA", while the input polymer "ABAA" becomes "ACBDAXA". Both of those output strings have the same 2-character pairs of internal strings, namely ("AC" "AX" "BD" "CB" "DA" "XA"). Therefore, the strings themselves don't actually matter; it's just the number of each character pair in the string.

With that understanding, let's get to the code.


Parts 1 and 2

First off, we'll parse the input. I took a slightly different approach from my usual strategy of parsing everything into individual lines, based on some tricks I've seen in previous Advent solutions from the Clojure community. So today, I still use my split-blank-line function to separate the first line (the template) from the multi-line rules. But this time, I use re-seq on all of the rule lines together, taking apart just the words. Given that long sequence, I call (partition 2 words) to collect them into pairs, then map each list pair into a vector, so I can collect them all into a map. The result of parsing the input is a two-element vector of the initial template and the map of 2-character strings to the 1-character string to inject between them.

(defn parse-input [input]
  (let [[template rules] (u/split-blank-line input)]
    [template
     (->> (re-seq #"\w+" rules) (partition 2) (map vec) (into {}))]))

Now really quickly, I'm going to make a helper function called update-add, which sort of acts like a map with a default value, since we saw in Day 12 that if you try to update a map by calling inc or +, when the current key is not in the map, you'll get a NullPointerException. So update-add lets us still update the map by essentially allowing the default value to be a zero. We'll use this several times in the solution.

(defn update-add [m k n] (update m k #(+ (or % 0) n)))

Next we'll create the function apply-rules, which takes in the rule set and the frequencies of string pairs. It's just a simple use of reduce, but with a heck of a deconstructor on its input. Remember that our freqs function argument is a map of structure {pair1 c1, pair2 c2} or {[a1 b1] c1, [a2 b2] c2}. Since maps are themselves sequences, we can think of the map as a big sequence of key-value pairs, like ([pair1 c1], [pair2 c2]), which again looks like ([[a1 b1] c1], [[a2 b2] c2]). Thus because the reducing function takes an accumulated value and the next pair in the freqs map, we deconstruct it to (fn [acc [[a b :as word] n]]).

With that ready to go, the reduction is simple. We find the character c in the rules for our a-b word, and in the next generation each of those pairs will translate into the same number of ac and cb pairs. So we'll use our handy update-add function to add n instances of both strings to the accumulating map of frequencies.

(defn apply-rules [rules freqs]
  (reduce (fn [acc [[a b :as word] n]] (let [c (rules word)]
                                         (-> acc
                                             (update-add (str a c) n)
                                             (update-add (str c b) n))))
          {} freqs))

Alright, let's work out the score for a given "string," which again we are representing as a frequency map. To start, we need to convert the frequency map of string pairs to the frequency map of individual characters. To do that, we must recognize that if we had the string aabaab, the map would be {"aa" 2, "ab" 2, "ba" 1}. Looking at just the first three characters, aab, we note that the second character of the first pair is the first character of the second pair, so we don't want to count both letters within each pair. We just need to count the first of each pair, so long as we add in the very last letter of the original input, since that will never change from generation to generation. Once we have that value, named below char-freqs, we need to find the largest and smallest frequency; note again that we don't care about the characters themselves, so we can call (sort-by - (vals char-freqs)) to pull out the frequency counts using vals before sorting from largest to smallest. Finally, we use juxt to grab the first and last values (largest and smallest, respectively) and subtract the two.

(defn score [initial-template freqs]
  (let [char-freqs (reduce (fn [acc [[a] n]] (update-add acc a n))
                           {(last initial-template) 1}
                           freqs)
        sorted-instances (sort-by - (vals char-freqs))]
    (apply - ((juxt first last) sorted-instances))))

We're in the home stretch, because it's time for our solve function! First we'll parse the input, and then we need to transform the polymer template into the initial frequency map by pairing together all of the characters before calling frequencies. Once we have that map, we'll use iterate to make an infinite sequence of new frequency maps for each generation, skip to the correct generation, and score it. Remember that we need to pass in the original template to the score function so we get that last character.

(defn solve [input step]
  (let [[template rules] (parse-input input)
        initial-freqs (->> template (partition 2 1) (map (partial apply str)) frequencies)]
    (->> (iterate (partial apply-rules rules) initial-freqs)
         (drop step)
         first
         (score template))))

Now we just have to invoke the solve method with the generation number, which is 10 for part 1 and 40 for part 2.

(defn part1 [input] (solve input 10))
(defn part2 [input] (solve input 40))