Skip to content

Commit 9379bb1

Browse files
committed
Day 21 documentation
1 parent 54c4584 commit 9379bb1

File tree

5 files changed

+252
-1
lines changed

5 files changed

+252
-1
lines changed

README.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,5 @@ _Warning:_ I write long explanations. So... yeah.
2929
| 17 | [source](src/advent_2020_clojure/day17.clj) | [blog](docs/day17.md) |
3030
| 18 | [source](src/advent_2020_clojure/day18.clj) | [blog](docs/day18.md) |
3131
| 19 | [source](src/advent_2020_clojure/day19.clj) | TBD |
32-
| 20 | [source](src/advent_2020_clojure/day20.clj) | [blog](docs/day20.md) |
32+
| 20 | [source](src/advent_2020_clojure/day20.clj) and [cube-solver source](src/advent_2020_clojure/cube_solver.clj) | [blog](docs/day20.md) |
33+
| 21 | [source](src/advent_2020_clojure/day21.clj) | [blog](docs/day21.md) |

docs/day21.md

+141
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
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

Comments
 (0)