Skip to content

Commit 7241df6

Browse files
committed
Day 8
1 parent bf86a93 commit 7241df6

File tree

6 files changed

+440
-0
lines changed

6 files changed

+440
-0
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@
99
| 5 | [source](src/advent_2021_clojure/day05.clj) | [blog](docs/day05.md) |
1010
| 6 | [source](src/advent_2021_clojure/day06.clj) | [blog](docs/day06.md) |
1111
| 7 | [source](src/advent_2021_clojure/day07.clj) | [blog](docs/day07.md) |
12+
| 8 | [source](src/advent_2021_clojure/day08.clj) | [blog](docs/day08.md) |

docs/day08.md

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

Comments
 (0)