|
| 1 | +# Day Five: |
| 2 | + |
| 3 | +* [Problem statement](https://adventofcode.com/2020/day/5) |
| 4 | +* [Solution code](https://github.com/abyala/advent-2020-clojure/blob/master/src/advent_2020_clojure/day05.clj) |
| 5 | + |
| 6 | +--- |
| 7 | + |
| 8 | +## Part 1 |
| 9 | + |
| 10 | +I rather liked today's puzzle - simple problem, no tricky surprises. Also, I think I've already |
| 11 | +solved it four or five ways, so I like the variety of options! |
| 12 | + |
| 13 | +We're looking for our seat on a plane, given instructions on how to get the row and column based on |
| 14 | +an input String. Of the 128 rows (0-127) and 8 columns (0-7), we split the rows in half and go to |
| 15 | +the front half (0-63) if the first character is `F` or the back half (64-127) if the first character |
| 16 | +is `B`. Then repeat for a total of 7 times until we find the right row. Then do the same for the |
| 17 | +last 3 characters using `L` for low and `R` for high to find the column. Then the seat ID is |
| 18 | +the row * 8 plus the column. |
| 19 | + |
| 20 | +So first, let's split each 10-character seat assignment into two strings - the first 7 and the last 3. |
| 21 | +We could manually create a vector using `[(take 7 seat) (drop 7 seat)]`, but `(split-at 7 seat)` |
| 22 | +does the work for us. This will create a vector with two char sequences. |
| 23 | + |
| 24 | +```clojure |
| 25 | +(defn split-seat [seat] (split-at 7 seat)) |
| 26 | +``` |
| 27 | + |
| 28 | +Next, we need to figure out how to find the correct row or column using the so-called "binary |
| 29 | +space partition" algorithm. The easiest solution I could find involves treating the front and back |
| 30 | +portions of the seat as binary numbers - if we convert `F` and `L` into `0`, and `B` and `R` into `1`, |
| 31 | +then these Strings convert beautifully. From the test cases, `FBFBBFF` should resolve to row 44, |
| 32 | +which is `0101100`. Similarly, `RLR` is 5, which in binary is `101`. |
| 33 | + |
| 34 | +Let's write a little helper function that converts a character into either a zero or one. Once |
| 35 | +again, we can use `(if (a-set test))` as a boolean expression to see if `test` is a member of the |
| 36 | +set `a-set`. |
| 37 | + |
| 38 | +```clojure |
| 39 | +(defn to-binary-digit [c] (if (#{\F \L} c) 0 1)) |
| 40 | +``` |
| 41 | + |
| 42 | +Now we have to do a little string manipulation to convert a sequence of characters into its integer |
| 43 | +value. We again use `as->` since we'll be mixing the collection operations `map` and `apply`, which |
| 44 | +use thread-last, with a non-collection operation that uses thread-first. The `binary-space-partition` |
| 45 | +function maps each digit to its binary form, then concatenates the sequence into one big string of |
| 46 | +zeros and ones, and then leverages Java's `Integer.parseInt` method with a radix of `2` to |
| 47 | +convert the String as binary into an integer. |
| 48 | + |
| 49 | +```clojure |
| 50 | +(defn binary-space-partition [instructions] |
| 51 | + (as-> instructions x |
| 52 | + (map to-binary-digit x) |
| 53 | + (apply str x) |
| 54 | + (Integer/parseInt x 2))) |
| 55 | +``` |
| 56 | + |
| 57 | +Calculating a seat ID is now a fairly trivial task. We split the 10-digit string into the front |
| 58 | +and back halfs using `split-seat`, and our `let` statement immediately destructures this vector |
| 59 | +into two variables `[r c]` for row and column. Then we multiply the binary form of the row by 8, |
| 60 | +and add it to the binary form of the column. |
| 61 | + |
| 62 | +```clojure |
| 63 | +(defn seat-id [seat] |
| 64 | + (let [[r c] (split-seat seat)] |
| 65 | + (-> (* (binary-space-partition r) 8) |
| 66 | + (+ (binary-space-partition c))))) |
| 67 | +``` |
| 68 | + |
| 69 | +Finally, to finish part 1, we need to find the largest seat ID. A little threading gets us the |
| 70 | +answer. |
| 71 | + |
| 72 | +```clojure |
| 73 | +(defn part1 [input] |
| 74 | + (->> (str/split-lines input) |
| 75 | + (map seat-id) |
| 76 | + (apply max))) |
| 77 | +``` |
| 78 | + |
| 79 | +--- |
| 80 | + |
| 81 | +## Part 2 |
| 82 | + |
| 83 | +For part 2, we need to find which seat ID is not filled. The instructions say that the seat IDs |
| 84 | +from the input make a contiguous list, but there's one seat missing. It won't be the lowest or |
| 85 | +highest value, so there's one value in the list such that the seat ID one higher does not exist. |
| 86 | + |
| 87 | +Again, there are lots of ways of doing this, but I think this is a good case for the `reduce` |
| 88 | +function. We'll make a function `missing-within-collection`, which takes a collection of numeric |
| 89 | +values and finds the first missing value. The input to `reduce` will be `(sort coll)`, so we |
| 90 | +can read through the list once instead of treating it as a set. Then the function to apply should |
| 91 | +look at the previous value calculated and the next value in the list. If `next` is one greater |
| 92 | +than `previous`, then we haven't found it yet, so the reducing function should return `next`. |
| 93 | +However, if `next` is not one greater than `previous`, then we can tell the `reduce` function that |
| 94 | +we've found our answer and it can stop doing its calculations. The `reduced` function accomplishes |
| 95 | +just that. This is `reduce`'s equivalent of applying a `break` within a loop, or an early `return` |
| 96 | +within a method. |
| 97 | + |
| 98 | +```clojure |
| 99 | +(defn missing-within-collection [coll] |
| 100 | + (reduce (fn [prev v] |
| 101 | + (let [target (inc prev)] |
| 102 | + (if (= v target) |
| 103 | + v ; This isn't the answer. Keep reducing. |
| 104 | + (reduced target)))) ; We found it! Short-circuit with the value we didn't find. |
| 105 | + (sort coll))) |
| 106 | +``` |
| 107 | + |
| 108 | +And then, of course, I always want my `part1` and `part2` functions to be simple if at all possible. |
| 109 | +Both functions require reading the input data, converting each value into its seat ID, and then |
| 110 | +doing something to it. So we'll pull out most of the logic from the old `part1` into a new function |
| 111 | +called `apply-to-seat-ids`, which takes in the input data and the function to apply to collection |
| 112 | +of seat IDs. This should wrap everything up cleanly. |
| 113 | + |
| 114 | +```clojure |
| 115 | +(defn apply-to-seat-ids [input f] |
| 116 | + (->> (str/split-lines input) |
| 117 | + (map seat-id) |
| 118 | + f)) |
| 119 | + |
| 120 | +(defn part1 [input] |
| 121 | + (apply-to-seat-ids input (partial apply max))) |
| 122 | + |
| 123 | +(defn part2 [input] |
| 124 | + (apply-to-seat-ids input missing-within-collection)) |
| 125 | +``` |
0 commit comments