|
| 1 | +# Day Eight: Seven Segment Search |
| 2 | + |
| 3 | +* [Problem statement](https://adventofcode.com/2021/day/8) |
| 4 | +* [Solution code](https://github.com/abyala/advent-2021-clojure/blob/master/src/advent_2021_clojure/day08.clj) |
| 5 | + |
| 6 | +--- |
| 7 | + |
| 8 | +## Part 1 |
| 9 | + |
| 10 | +Today's puzzle brought me back to my high school days, and the old clock radio I had beside my bed. We're reading |
| 11 | +jumbled up values on a numeric display, where our goal is to determine which strings refer to which numbers on the |
| 12 | +display. Let's do it. |
| 13 | + |
| 14 | +First, let's check out the input. We get a sequence of lines, where each line has two groups of space-separated |
| 15 | +strings, each group separated by a pipe. Each letter within a string represents one of the seven segments of on the |
| 16 | +display, so we actually care about the set of letters within each string, rather than the string itself. Thus we will |
| 17 | +make three small parse functions that fit together: |
| 18 | + |
| 19 | +1. `parse-component` will take in one half of a line (space-separated strings), and convert them into a sequence of |
| 20 | +sets of letters; in future, we will refer to each letter as a _signal_. Thus the string `abc ad` becomes |
| 21 | +`(#{\a \b \c} #{\a \d})`. |
| 22 | +2. `parse-line` splits an entire line by the pipe symbol, and returns a two-element sequence of parsed components; in |
| 23 | +future, we will refer to the first element as the signal pattern or pattern, and the second element as the output value |
| 24 | +or output. Thus the line `abc ad | a bc` would become `((#{\a \b \c} #{\a \d}) (#{\a} #{\b \c))`. |
| 25 | +3. `parse-input` just calls `parse-line` for each line. The final data structure is a sequence of sequences of patterns |
| 26 | +and outputs. More explicitly, the data structure is a sequence of two-element sequences, of sequences of sets of |
| 27 | +characters. It's best not to sound such things out because it's much easier to work with this data than explicitly |
| 28 | +define its type. |
| 29 | + |
| 30 | +```clojure |
| 31 | +(defn parse-component [component] |
| 32 | + (map set (str/split component #" "))) |
| 33 | + |
| 34 | +(defn parse-line [line] |
| 35 | + (->> (str/split line #"\|") |
| 36 | + (map (comp parse-component str/trim)))) |
| 37 | + |
| 38 | +(defn parse-input [input] |
| 39 | + (map parse-line (str/split-lines input))) |
| 40 | +``` |
| 41 | + |
| 42 | +For part one, we need to count the number of outputs that correspond to the digit 1, 4, 7, or 8. These digits are |
| 43 | +special because each is the only digit with that number of segments - 1 has two digits, 4 has four, 7 has three, and |
| 44 | +8 and seven. So what we'll do first is define `unique-lengths` as the set of lengths that correspond to a single digit, |
| 45 | +this being `#{2, 3, 4, 7}`. Then we'll parse the input into each line, pull together all of the digits together, |
| 46 | +filter out only the ones whose length is one of the `unique-lengths`, and count them up. `mapcat` is Clojure's |
| 47 | +implementation of `flatmap`. Thus in one step it will `map` through every line to its digits by using the `second` |
| 48 | +function (remember that `first` are the patterns and `seconds` are the digits), and then it will `cat` all of those |
| 49 | +sets together. |
| 50 | + |
| 51 | +```clojure |
| 52 | +(def unique-lengths #{2 3 4 7}) |
| 53 | +(defn part1 [input] |
| 54 | + (->> (parse-input input) |
| 55 | + (mapcat second) |
| 56 | + (filter #(-> % count unique-lengths)) |
| 57 | + count)) |
| 58 | +``` |
| 59 | + |
| 60 | +I'll make a quick note here about Clojure's approach to sets and maps. Both of these data structures can act as |
| 61 | +functions over their elements. So `(#{:a :b :c} :b)` and `(:b #{:a :b :c})` both mean "return `:b` if it's an |
| 62 | +element of the set `#{:a :b :c}`, or else return `nil`." In this case, it will return `:b`, which is itself a truthy |
| 63 | +value. So you can do cool things like `(if (#{:a :b :c} :b) "present" "absent")` to use the set itself as a predicate. |
| 64 | +Even cooler, you can say `(filter #{:a :b} [:a :b :c :d :e])`, which returns `(:a :b)`, because `filter` will only |
| 65 | +return the values in the collection that appear within the set. Really cool. |
| 66 | + |
| 67 | +So in `part1`, after we call `mapcat` we have a sequence of sets of characters. The `filter` function takes each set |
| 68 | +of characters, calls `count` to get the number of characters within the set, and calls `(unique-lengths n)` where the |
| 69 | +former is a set. The collection `(#{:a :b} #{:a} #{:a :b :c :d})` would come back as `(#{:a :b} #{:a :b :c :d})` |
| 70 | +because the strings have lengths of 2, 1, and 4, and only 2 and 4 are in the `unique-lengths` set. |
| 71 | + |
| 72 | +--- |
| 73 | + |
| 74 | +## Part 2 |
| 75 | + |
| 76 | +Part 2 was neat because the code itself wasn't very complex, but this was a pure logic puzzle. For each line of |
| 77 | +patterns and outputs, we need to determine which character corresponds to which segment of the clock, then use that to |
| 78 | +map each of the outputs into their digits. Each row has four digits, so convert each row into its four-digit number, |
| 79 | +and add them all together. We don't actually care which _individual_ letter corresponds to which segment; it's more |
| 80 | +important to know which _set_ of letters correspond to which numeric digit, and that's actually not too bad. |
| 81 | + |
| 82 | +Let's ignore the code for now, and just work through the problem logically. |
| 83 | +* 1, 4, 7, and 8 are easy enough to spot, because each of their patterns have a unique number of letters. |
| 84 | +* We then move on to 0, 6, and 9, because those three each have six letters. |
| 85 | + * Both 0 and 9 have both of the letters on the right-hand side of the digit, which combined make up the digit 1. |
| 86 | + Thus, the 6 is the word within 0, 6, and 9 which does _not_ have all of the letters that the 1 does. |
| 87 | + * Similarly, 9 is the only number that has all of the segments that the 4 has, so we can pick that out. |
| 88 | + * Of the three of these, the one that isn't the 6 or the 9 is the 0. |
| 89 | +* Then we move on to the last three numbers, 2, 3, and 5, which each have a length of 5 letters. |
| 90 | + * We know which letters make up the 6, and 5 is the only digit that is a distinct subset of the 6, as they only |
| 91 | + differ in the bottom-left segment. |
| 92 | + * Similarly, 3 is the only digit that has both segments that make up the 1. |
| 93 | + * The last string in the collection must be the 2. |
| 94 | + |
| 95 | +Once we have that all identified, the code to implement `identify-signals` is pretty easy. Purely for the sake of |
| 96 | +clarity, I used `letfn` to make four helper functions, so we think in business terms. |
| 97 | +* `only-subset` takes in a set of letters and a collection of patterns, and returns the one and only pattern where |
| 98 | + where the letters are a subset of the pattern. |
| 99 | +* `only-superset` does the same thing, except that the first argument is a superset of the pattern. |
| 100 | +* `only-not-subset` returns the only pattern where the first argument is _not_ a subset. |
| 101 | +* `leftover` just returns the only element of the collection which is not in the set of strings in the first argument. |
| 102 | + |
| 103 | +Thus armed, we call `(group-by count signal-patterns)` to create a map of `{pattern-length (patterns)}`, and bind |
| 104 | +each digit as we work through the problem. At the end, we return a nice map of each set of strings to its numeric |
| 105 | +digit. |
| 106 | + |
| 107 | +```clojure |
| 108 | +(defn identify-signals [signal-patterns] |
| 109 | + (letfn [(only-subset [s coll] (first (filter (partial set/subset? s) coll))) |
| 110 | + (only-superset [s coll] (first (filter (partial set/superset? s) coll))) |
| 111 | + (only-not-subset [s coll] (first (remove (partial set/subset? s) coll))) |
| 112 | + (leftover [vals coll] (first (remove vals coll)))] |
| 113 | + (let [patterns (group-by count signal-patterns) |
| 114 | + ; Unique lengths |
| 115 | + one (first (patterns 2)) |
| 116 | + four (first (patterns 4)) |
| 117 | + seven (first (patterns 3)) |
| 118 | + eight (first (patterns 7)) |
| 119 | + |
| 120 | + ; 0, 6, and 9 all have length of 6 |
| 121 | + zero-six-nine (patterns 6) |
| 122 | + six (only-not-subset one zero-six-nine) |
| 123 | + nine (only-subset four zero-six-nine) |
| 124 | + zero (leftover #{six nine} zero-six-nine) |
| 125 | + |
| 126 | + ; 2, 3, and 5 all have length of 5 |
| 127 | + two-three-five (patterns 5) |
| 128 | + five (only-superset six two-three-five) |
| 129 | + three (only-subset one two-three-five) |
| 130 | + two (leftover #{three five} two-three-five)] |
| 131 | + {zero 0, one 1, two 2, three 3, four 4, five 5, six 6, seven 7, eight 8, nine 9}))) |
| 132 | +``` |
| 133 | + |
| 134 | +It's smooth sailing from here. We'll make a `find-digits` function that takes in the signal map from above and the |
| 135 | +list of output strings, and returns the numeric digit value. For this, we start with `(map signals outputs)`, which |
| 136 | +this time uses a map as a function instead of a set. So `(map {:a 1, :b 2, :c 3} [:a :c])` yields `(1 3)`. After we |
| 137 | +use the `map` function to apply the `signals` map to the output sequence, we get a sequence of ints, so we put them |
| 138 | +into a string and parse them into a new integer to get the actual numeric value. |
| 139 | + |
| 140 | +```clojure |
| 141 | +(defn find-digits [signals outputs] |
| 142 | + (->> (map signals outputs) |
| 143 | + (apply str) |
| 144 | + (parse-int))) |
| 145 | +``` |
| 146 | + |
| 147 | +Finally, we write the `part2` function. Here we'll parse the input, identify the signals from the patterns, feed them |
| 148 | +in to `find-digits` with the `outputs`, and add together the results. See? That wasn't so bad! |
| 149 | + |
| 150 | +```clojure |
| 151 | +(defn part2 [input] |
| 152 | + (->> (parse-input input) |
| 153 | + (map (fn [[patterns outputs]] (-> (identify-signals patterns) |
| 154 | + (find-digits outputs)))) |
| 155 | + (apply +))) |
| 156 | +``` |
0 commit comments