|
| 1 | +# Day Twenty-One: Allergen Assessment |
| 2 | + |
| 3 | +* [Problem statement](https://adventofcode.com/2020/day/21) |
| 4 | +* [Solution code](https://github.com/abyala/advent-2020-clojure/blob/master/src/advent_2020_clojure/day21.clj) |
| 5 | + |
| 6 | +--- |
| 7 | + |
| 8 | +Today was a pretty straightforward puzzle, especially considering how complex the past few days of projects |
| 9 | +have been. Because this mostly deals with playing around with map and set operations, I don't think I'm |
| 10 | +going to find ways to simplify today's problem. Let's just call it an exercise in stamping out the code. |
| 11 | + |
| 12 | +Almost all of the work goes into part 1, so I'll explain the work in there. |
| 13 | + |
| 14 | +--- |
| 15 | + |
| 16 | +## Part 1 |
| 17 | + |
| 18 | +We are given a data file of foods, where each food is a list of space-separated unrecognizable ingredients, |
| 19 | +and one or more allergens. The goal is to find which ingredients are not allergens, and return the sum of |
| 20 | +all times said ingredients appear in a food. |
| 21 | + |
| 22 | +Again, we're going to do a little extra processing. Sorry! |
| 23 | + |
| 24 | +First off, let's handle data parsing. My initial thought was to make a `parse-food` function, which reads |
| 25 | +in a single line and returns a map of each allergen to the set of ingredients in which |
| 26 | +it might be found. So if the food were cheesecake and were defined as |
| 27 | +`cheese cinnamon flour magic (contains dairy, gluten)`, the result would be |
| 28 | +`{"dairy" #{"cheese" "cinnamon" "flour" "magic"}, "gluten" #{"cheese" "cinnamon" "flour" "magic"}}`. |
| 29 | + |
| 30 | +Then `parse-input` just returns a sequence all of those maps found in the input file. |
| 31 | + |
| 32 | +```clojure |
| 33 | +(defn parse-food [line] |
| 34 | + (let [[_ ingredient-str allergen-str] (re-matches #"(.*) \(contains (.*)\)" line) |
| 35 | + ingredients (set (str/split ingredient-str #" ")) |
| 36 | + allergens (set (str/split allergen-str #", "))] |
| 37 | + (->> (map #(vector % ingredients) allergens) |
| 38 | + (into {})))) |
| 39 | + |
| 40 | +(defn parse-input [input] |
| 41 | + (->> input str/split-lines (map parse-food))) |
| 42 | +``` |
| 43 | + |
| 44 | +The bulk of the work appears in `ingredients-with-allergens`, which takes in the parsed |
| 45 | +sequence of allergens mapped to their set of potential ingredient culprits, and returns |
| 46 | +a map of each allergen pointing to its actual ingredient. We're going to do a |
| 47 | +`loop-recur`, but we need to do a little more data conditioning first. The idea is that |
| 48 | +among all of the allergens, we assume that at least one of them appears connected to |
| 49 | +only one ingredient. At that point, we've identified the connection between the |
| 50 | +ingredient and the allergen, and we can assume that none of the other allergens are |
| 51 | +associated to the same ingredient. |
| 52 | + |
| 53 | +Since `parse-food` returns a sequence of maps, one |
| 54 | +for each food, mapping each allergen to the potential ingredients, we want to combine |
| 55 | +all of these maps together. The `merge-with` function comes to the rescue here, as that |
| 56 | +function combines the entries in multiple maps by applying a joining function. In this |
| 57 | +case, we're mapping the allergen to the set of potential ingredients, so for each food |
| 58 | +we want to look at the intersection of ingredients; if food A says that allergen |
| 59 | +`dairy` came from either `ingredient-a` or `ingredient-b`, and food B says that allergen |
| 60 | +`dairy` came from either `ingredient-a` or `ingredient-c`, we know that `ingredient-a` |
| 61 | +must be the ingredient as it applies to both rules. All of this can be accomplished |
| 62 | +very simply by calling `(apply merge-with set/intersection foods)`. |
| 63 | + |
| 64 | +Then the rest of the function performs a loop of all allergens without an associated |
| 65 | +ingredient and the list of identified allergen-ingredient pairs. Each time, we |
| 66 | +find an element in `unidentified` where the set of ingredients has only one value. |
| 67 | +With that pair defined, we recurse into the loop by removing the allergen from the |
| 68 | +`unidentified` map, and by also removing the ingredient from every other unidentified |
| 69 | +allergen's set of ingredients. |
| 70 | + |
| 71 | +```clojure |
| 72 | +(defn ingredients-with-allergens [foods] |
| 73 | + (loop [unidentified (apply merge-with set/intersection foods) |
| 74 | + identified {}] |
| 75 | + (if (empty? unidentified) |
| 76 | + identified |
| 77 | + (let [[allergen ingredient] (->> unidentified |
| 78 | + (keep (fn [[k v]] |
| 79 | + (when (= 1 (count v)) [k (first v)]))) |
| 80 | + first)] |
| 81 | + (recur (->> (dissoc unidentified allergen) |
| 82 | + (map (fn [pair] (update pair 1 #(disj % ingredient)))) |
| 83 | + (into {})) |
| 84 | + (assoc identified allergen ingredient)))))) |
| 85 | +``` |
| 86 | + |
| 87 | +Then we need another function that takes in the same sequence of maps of foods, and |
| 88 | +returns the number of times each ingredient appears in a food. Each map will contain |
| 89 | +1 or more entries, but they each have the same set of values, since the food has the |
| 90 | +same ingredients no matter which allergen we look at. So we can grab the first allergen, |
| 91 | +whatever it is, from each map, and grab its set value using |
| 92 | +`(map (partial (comp second first)) foods)` where `first` is the first map entry, and |
| 93 | +`second` is the value for that entry, i.e. the set of ingredients. With that done, we |
| 94 | +concatenate all of the sets into a big sequence, and call `frequencies` to get our result. |
| 95 | + |
| 96 | +```clojure |
| 97 | +(defn ingredient-frequencies [foods] |
| 98 | + (->> foods |
| 99 | + (map (partial (comp second first))) |
| 100 | + (apply concat) |
| 101 | + frequencies)) |
| 102 | +``` |
| 103 | + |
| 104 | +Finally, we're ready to solve part 1. We'll parse the data and calculate the food |
| 105 | +sequences and call `ingredients-with-allergens` to find out which ingredient contains |
| 106 | +which allergen. We want to throw away all of the ingredients associated to allergens, |
| 107 | +so we'll pull out the ingredients using `(map second allergens-to-foods)`, then |
| 108 | +dissociate them from the frequency map, leaving us with just a map of each "safe" |
| 109 | +ingredient and its frequency count. Then we just add it up to get the answer! |
| 110 | + |
| 111 | +```clojure |
| 112 | +(defn part1 [input] |
| 113 | + (let [foods (parse-input input) |
| 114 | + allergens-to-foods (ingredients-with-allergens foods) |
| 115 | + ingr-freqs (ingredient-frequencies foods)] |
| 116 | + (->> (map second allergens-to-foods) |
| 117 | + (reduce (partial dissoc) ingr-freqs) |
| 118 | + (map second) |
| 119 | + (apply +)))) |
| 120 | +``` |
| 121 | + |
| 122 | +--- |
| 123 | + |
| 124 | +## Part 2 |
| 125 | + |
| 126 | +As I already said, we did enough work for Part 1 and Part 2 requires almost no work. |
| 127 | +We need to sort the allergens by the names of their ingredients, and then combine them |
| 128 | +into a comma-separated string, and there's nothing to it. Parse the data and grab |
| 129 | +the map of allergens to ingredients. We can call `sort` on a map, which sorts the |
| 130 | +`[key value]` vectors, so we sort by the allergen (`first`) and then map out the |
| 131 | +ingredient (`second`). Finally, join the strings with `","` as the delimiter. Piece |
| 132 | +of cake! |
| 133 | + |
| 134 | +```clojure |
| 135 | +(defn part2 [input] |
| 136 | + (->> (parse-input input) |
| 137 | + ingredients-with-allergens |
| 138 | + (sort-by first) |
| 139 | + (map second) |
| 140 | + (str/join ","))) |
| 141 | +``` |
0 commit comments