Skip to content

Latest commit

 

History

History
189 lines (148 loc) · 8.29 KB

day08.md

File metadata and controls

189 lines (148 loc) · 8.29 KB

Day 8: Treetop Tree House


Part One

Today's puzzle starts with us reading a numeric grid that corresponds to trees, and our having to count the number of trees that are visible from at least one direction, where visibility means that there are no trees between it and the edge with equal or greater height.

To start, we need to parse the input string, and while this could easily come in as a 2-dimensional array, I just prefer working with maps. I did create a helper function char->int since Clojure doesn't have a built-in function for this conversion; in this case, we just leverage Java's Character class's static digit method.

; advent-2022-clojure.utils namespace
(defn char->int
  "Reads a numeric character and returns its integer value, assuming base 10."
  [^Character c]
  (Character/digit c 10))

; advent-2022-clojure.day08 namespace
(defn parse-input [input]
  (reduce (fn [acc [k v]] (assoc acc k (char->int v)))
          {}
          (p/parse-to-char-coords input)))

For the parser, we're going to use a reduce function. The input collection parse-to-char-coords comes from a helper namespace I've created in the past called advent-2022-clojure.point, since Advent project often work with x-y coordinates, and there's no reason to reinvent this. Given its return of a sequence of ([[x y] c]), we just assoc the points into the map, converting the characters to ints using char->int.

Perhaps the most important function in the solution is trees-in-direction, which returns a lazy sequence of all tree heights starting from a location and moving in one direction.

(defn trees-in-direction [points p dir]
  (->> (iterate #(mapv + dir %) p)
       (map points)
       (next)
       (take-while some?)))

The iterate function takes a starting value and returns a lazy infinite sequence of the starting value and the result of applying its first argument function to it. In this case, we're using (mapv + p dir) to map the x and y coordinates of the given point to the x and y coordinates of a direction, such as up being [0 1], by adding the values together. Then we map each coordinate to its value in the points map to see what resides at that location. After dropping the first value, since it's always the same as the initial value sent to the iterate function, we just take all of the non-nil values by calling (take-while some?).

So now that we've parse the input into a map of coordinates to their tree heights, and we can start from any position and see the trees going out in any direction, we need to implement visible? to find out how many of them are visible from any direction.

(defn visible?
  ([points pos] (some #(visible? points pos %) p/cardinal-directions))
  ([points pos dir]
   (let [p (points pos)]
     (every? #(< % p) (trees-in-direction points pos dir)))))

The function has two arities for convenience. The 3-arity version has to make sure that every tree in a direction is strictly shorter than the starting tree, so every? does that. Then the 2-arity version calls the 3-arity version to see if there is any direction that is visible. Note that we must use some instead of any? here because the latter returns true for every input.

It's time to wrap this up with our part1 function.

(defn part1 [input]
  (let [points (parse-input input)]
    (count-if #(visible? points %) (keys points))))

This is quite a simple function now. We use (keys points) to extract all of the coordinates in the map, and use our fancy new count-if function to count how many of those coordinates pass the visible? predicate.


Part Two

The second part is very similar to the first, and leverages much of the code. We need to find the point with the greatest viewing distance, meaning the product of the number of trees the viewer can see from each direction.

(defn viewing-distance
  ([points pos] (transduce (map #(viewing-distance points pos %)) * p/cardinal-directions))
  ([points pos dir]
   (let [p (points pos)]
     (reduce (fn [acc t] (if (>= t p) (reduced (inc acc)) (inc acc)))
             0
             (trees-in-direction points pos dir)))))

Like visible?, the viewing-distance function also has two arities. There are several ways to implement the short-circuit feature of the 3-arity function, but I figured that a reduce function with a reduced call is the most intuitive option. For each tree in a direction, the viewer can definitely see it, so we always call (inc acc). But if the tree blocks the view, we stop there with reduced, and if not, we keep going.

Then, as we tend to do in Advent this year, we use the transduce function again! Here we feed in the four cardinal directions, and transform them into their viewing distances from the starting point. Finally, we reduce the values down with the * multiplication function.

Then we just need part2 and we're done.

(defn part2 [input]
  (let [points (parse-input input)]
    (transduce (map (partial viewing-distance points)) max 0 (keys points))))

Well would you look at that -- it's another transducer! For each coordinate, we map it to its viewing distance, and reduce it down to the max value. We do have to provide an initial value, because otherwise Clojure would try to call (max) without any input, and there is no 0-arity version of the function. This contrasts the initial value 0 for +, or 1 for *; there is no sensible starting value for max.


Refactorings

Introducing parse-to-char-coords-map

Advent problems often involve playing with points in a grid, so I figured I'd add a few more utility functions to the advent-2022-clojure.point namespace, mostly around parsing. First, I enhanced the parse-to-char-coords function to accept an optional argument on what function to map each character on the way in. I could envision doing something like (parse-to-char-coords {\. :space \# :wall} input) for a maze.

(defn parse-to-char-coords
  ([input] (parse-to-char-coords identity input))
  ([f input] (->> (str/split-lines input)
                  (map-indexed (fn [y line]
                                 (map-indexed (fn [x c] [[x y] (f c)]) line)))
                  (apply concat))))

Then I finally implemented a generic parse-to-char-coords-map function, also with two arities, but which returns a map of coordinates to the (potentially transformed) character at that coordinate. As with today's puzzle, I often find maps easier to work with over sequences or multidimensional vectors.

(defn parse-to-char-coords-map
  ([input] (parse-to-char-coords-map identity input))
  ([f input] (into {} (parse-to-char-coords f input))))

This means that the parse-input function can get simplified into a very clean 1-liner.

(defn parse-input [input]
  (p/parse-to-char-coords-map char->int input))

Introducing take-until

I saw several of my coworkers and some Clojurians using some form of a take-until function, so I decided to give it a shot too. Here's my implementation.

(defn take-until [pred coll]
  "Returns all values in the input collection for which the predicate is falsey, plus the first one that is truey (if
  any). Returns nil for a nil or empty input collection."
  (when (seq coll)
    (let [[x & xs] coll]
      (if (pred x) (list x) (lazy-seq (cons x (take-until pred xs)))))))

This implementation returns a lazy sequence of values from an incoming predicate, up to and including the first value where the predicate returns a truthy response.

The new take-until function really simplifies the viewing-distance function by getting rid of that reduce call with the reduced short-circuit. Instead, once we have the sequence of trees, we take values out until we reach the end or one of them is at least as high as the source tree. Then at that point, we just need to call count for our answer.

(defn viewing-distance
  ([points pos] (transduce (map #(viewing-distance points pos %)) * p/cardinal-directions))
  ([points pos dir]
   (let [p (points pos)]
     (->> (trees-in-direction points pos dir)
          (take-until (partial <= p))
          (count)))))