|
| 1 | +# Day Sixteen: Ticket Translation |
| 2 | + |
| 3 | +* [Problem statement](https://adventofcode.com/2020/day/16) |
| 4 | +* [Solution code](https://github.com/abyala/advent-2020-clojure/blob/master/src/advent_2020_clojure/day16.clj) |
| 5 | + |
| 6 | +--- |
| 7 | + |
| 8 | +Today's problem was a matter of parsing a complex incoming string, and then doing a bunch of set manipulation, maps, |
| 9 | +and filters. To be honest, I didn't find this one very entertaining, so I'm just going to quickly run through what |
| 10 | +each function does, show how the shape of the data changes from function to function, and call it a day. |
| 11 | + |
| 12 | +--- |
| 13 | + |
| 14 | +## Part 1 |
| 15 | + |
| 16 | +The `parse-input` function parses the incoming String into my state object. I leverage the `utils/split-blank-line-seq` |
| 17 | +function from a few problems ago, which converts the giant String into three components -- one for the rules, the |
| 18 | +"your ticket" section, and the "nearby tickets" section. Then it's a little regex and a whole lot of Integer parsing. |
| 19 | +The output shape is a map, that definitely could be simplified if I were so inclined. |
| 20 | + |
| 21 | +```clojure |
| 22 | +; Output shape |
| 23 | +{:fields [["name" [low1 high1] [low2 high2]]] |
| 24 | + :my-ticket [1 2 3 4] |
| 25 | + :nearby-tickets [[1 2 3] [4 5 6] [7 8 9]]} |
| 26 | +``` |
| 27 | + |
| 28 | +The `invalid-fields` function looks at all of the fields and combines them into a single list of `[low high]` pairs. |
| 29 | +Then the `flatten` function fully flattens the nearby tickets into a single list of integer values, which we filter |
| 30 | +down using `not-any?` to return only the values that aren't within any of the ranges. The output shape is a simple |
| 31 | +list of integers, representing the invalid fields. |
| 32 | + |
| 33 | +```clojure |
| 34 | +(defn invalid-fields [{:keys [fields nearby-tickets]}] |
| 35 | + (let [every-range (->> (map rest fields) (apply concat))] |
| 36 | + (->> (flatten nearby-tickets) |
| 37 | + (filter (fn [fld] (not-any? (fn [[x y]] (<= x fld y)) every-range)))))) |
| 38 | +``` |
| 39 | + |
| 40 | +Then `part1` just adds together all invalid fields. |
| 41 | + |
| 42 | +```clojure |
| 43 | +(defn part1 [input] |
| 44 | + (->> input parse-input invalid-fields (apply +))) |
| 45 | +``` |
| 46 | + |
| 47 | +--- |
| 48 | + |
| 49 | +## Part 2 |
| 50 | + |
| 51 | +Part 2 was a little trickier to solve, and my answer is a bit wordy. But I wanted to focus a bit more on transforming |
| 52 | +the data from one state to another, rather than doing everything inline as a giant list. So the solution is a bunch |
| 53 | +of small transformations. |
| 54 | + |
| 55 | +The overall strategy was to rip apart the complexity of the ticket structure. So I wanted to make a map of sets. To |
| 56 | +start, every ticket field was mapped to the set of all possible field indices. Then we go through every nearby ticket, |
| 57 | +and for every field type in which the ticket field doesn't fit, remove that index from the field type's set of |
| 58 | +indices. By removing every rule violation, we should be left with only the field types and the indexes for which all |
| 59 | +tickets validate. So with that... |
| 60 | + |
| 61 | +`valid-tickets-only` takes in the full state, and returns only the nearby tickets that are valid, meaning that they |
| 62 | +don't have any of the fields described previously in the `invalid-fields` function. This returns all of the remaining |
| 63 | +tickets in their normal form, so `[[1 2 3] [4 5 6] [7 8 9]]`. |
| 64 | + |
| 65 | +```clojure |
| 66 | +(defn valid-tickets-only [{:keys [nearby-tickets] :as parsed}] |
| 67 | + (let [bad-fields (-> parsed invalid-fields set)] |
| 68 | + (filterv (fn [t] (not-any? #(bad-fields %) t)) |
| 69 | + nearby-tickets))) |
| 70 | +``` |
| 71 | + |
| 72 | +The next function, `ticket-pairs`, rips out every `[idx v]` tuple across all of the nearby tickets, since I found that |
| 73 | +easier to reason with than keeping the original vector of vectors. So this function takes in the ticket list, of shape |
| 74 | +`[[1 2 3] [4 5 6] [7 8 9]]` and returns a simple `[[0 1] [1 2] [2 3] [0 4] [1 5] [2 6] [0 7] [1 8] [2 9]]` vector. |
| 75 | + |
| 76 | +```clojure |
| 77 | +(defn ticket-pairs [tickets] |
| 78 | + (mapcat (fn [ticket] (map-indexed (fn [idx v] (vector idx v)) ticket)) |
| 79 | + tickets)) |
| 80 | +``` |
| 81 | + |
| 82 | +The first beefy function is `possible-indexes`, with its coordinating function `all-possible-indexes`. |
| 83 | +`possible-indexes` takes in the list of all `ticket-pairs` and the total number of fields in the tickets, and then |
| 84 | +a single `field` rule definition of form `["name" [low1 high1] [low2 high2]]` from problem state. We run a reducing |
| 85 | +function, starting with a set of all possible indexes, and then we look at every ticket pair. If that pair's value |
| 86 | +doesn't comply with the rule, remove that pair's index from the set of possible indexes. There's a whole bunch of |
| 87 | +extra calculations going on, but this algorithm is still very fast. As a reminder to my future self -- we use `dissoc` |
| 88 | +to remove a mapping from a map, but `disj` to remove a value from a set. Why one function can't do both... |
| 89 | + |
| 90 | +Then `all-possible-fields` just maps each field to the set of possible indexes, and throws it all into a map. |
| 91 | + |
| 92 | +```clojure |
| 93 | +(defn possible-indexes [ticket-pairs num-fields [name [low1 high1] [low2 high2]]] |
| 94 | + (->> (reduce (fn [acc [idx v]] |
| 95 | + (if (or (<= low1 v high1) (<= low2 v high2)) |
| 96 | + acc |
| 97 | + (disj acc idx))) |
| 98 | + (set (range num-fields)) |
| 99 | + ticket-pairs) |
| 100 | + (vector name))) |
| 101 | + |
| 102 | +(defn all-possible-indexes [fields nearby-ticket-pairs] |
| 103 | + (->> fields |
| 104 | + (map #(possible-indexes nearby-ticket-pairs (count fields) %)) |
| 105 | + (into {}))) |
| 106 | +``` |
| 107 | +Armed with the possible fields, our goal is to use `field-mappings` to actually decide which field belongs to which |
| 108 | +index. We use a simple `loop-recur`, starting with all fields being `unsolved`, and moving them one-by-one into the |
| 109 | +`solved` set. In each loop, we find one field that only has one index that is valid and hasn't been claimed yet by |
| 110 | +another field. Knowing the field name and its singular index, we use `remove-all-traces` to eliminate that index |
| 111 | +from all remaining fields' set of indexes, and loop again. |
| 112 | + |
| 113 | +```clojure |
| 114 | +(defn remove-all-traces [possible-indexes index] |
| 115 | + (loop [fields possible-indexes, keys (keys possible-indexes)] |
| 116 | + (if-let [k (first keys)] |
| 117 | + (recur (update fields k #(disj % index)) |
| 118 | + (rest keys)) |
| 119 | + fields))) |
| 120 | + |
| 121 | +(defn field-mappings [fields nearby-ticket-pairs] |
| 122 | + (loop [unsolved (all-possible-indexes fields nearby-ticket-pairs) |
| 123 | + solved {}] |
| 124 | + (if (empty? unsolved) |
| 125 | + solved |
| 126 | + (let [[name actual-index] (->> unsolved |
| 127 | + (keep (fn [[name indexes]] |
| 128 | + (when (= 1 (count indexes)) |
| 129 | + [name (first indexes)]))) |
| 130 | + first)] |
| 131 | + (recur (-> unsolved |
| 132 | + (dissoc name) |
| 133 | + (remove-all-traces actual-index)) |
| 134 | + (assoc solved name actual-index)))))) |
| 135 | +``` |
| 136 | + |
| 137 | +Finally, `part2` puts the pieces together. Starting with the input, we parse it, identify the valid tickets, flatten |
| 138 | +them into ticket pairs, and identify the mappings. With the correct mappings in-place, we use a `keep-when` to pick |
| 139 | +the fields whose name starts with `departure`, map each to the value that that index in `my-ticket`, and multiple the |
| 140 | +values together. |
| 141 | + |
| 142 | +```clojure |
| 143 | +(defn part2 [input] |
| 144 | + (let [{:keys [fields my-ticket] :as parsed} (parse-input input) |
| 145 | + valid-tickets (valid-tickets-only parsed) |
| 146 | + nearby-ticket-pairs (ticket-pairs valid-tickets) |
| 147 | + mappings (field-mappings fields nearby-ticket-pairs)] |
| 148 | + (->> mappings |
| 149 | + (keep (fn [[name idx]] |
| 150 | + (when (str/starts-with? name "departure") (get my-ticket idx)))) |
| 151 | + (apply *)))) |
| 152 | +``` |
| 153 | + |
| 154 | +All in all, there's a bunch of code here, but it's all reasonably straightforward... now that it's done! |
0 commit comments