Skip to content

Latest commit

 

History

History
128 lines (103 loc) · 5.25 KB

day03.md

File metadata and controls

128 lines (103 loc) · 5.25 KB

Day Three: Toboggan Trajectory


Part 1

It's sledding time! Tobogganing time? I'm no longer a Yankee, so I can't tell the difference anymore.

In this problem, we're given an infinite horizontal map of open spaces and trees, and a path to go down a hill. The input is a map of open spaces (periods) and trees (hash signs), and a direction of moving right three spaces and down one, until we get below the input map. As stated above, the map repeats horizontally.

Usually, I start with a parse-input function, but I skipped that today. We'll just treat the input as a simple list of Strings using (str/split-lines). However, to keep our program context-relevant, I made a simple function tree? to check if a given character is a tree. I also made a constant origin for the starting point. Neither of these are critical, of course, but let's be clean.

Note that origin is a simple value, so we use def, while tree? is a function, so we use defn as the combination of def and fn.

(def origin [0 0])
(defn tree? [c] (= c \#))

Next, let's figure out every spot in the path that we will toboggan over. For this, we'll use the iterate function to generate an infinite sequence, and then figure out how to get out of it. iterate has a starting point, which will be the origin. The next spot we might hit requires taking the last starting point and adding the path vector. The easiest way to add together two vectors of integers is to call (map + v1 v2), which in this case would be (map + [0 0] [3 1]). Since the iterate function feeds in its last value as the last parameter into its function, the % sign in #(map + path %) is the previous calculation.

Then we just have to decide how many values to take out of the infinite sequence. take-while takes in a predicate and a sequence, and returns all values from the sequence until the first value does not return true from the predicate. We only want the [x y] vectors where y fits within the number of tree lines. I could use defined the predicate as (fn [[_ y]] (< y num-tree-lines)) but since there is only one parameter, we can use the #() short-hand and call (second %) to take the second value out of the input parameter sequence.

(defn target-coordinates [path num-tree-lines]
  (take-while
    #(< (second %) num-tree-lines)
    (iterate #(map + path %) origin)))

Now that we have a list of coordinates to hit, let's find out what's at each coordinate. To handle the horizontal scrolling, I could have applied the mod function to the x value for each point; if the width of the row is 10 characters and x=13, we could just look at the element at x=3. But that's no fun. Instead, the value-at function takes the parsed map of tree lines and the target coordinate, then it finds the correct row, and then we apply the cycle function. cycle takes a sequence and returns an infinite (lazy) sequence that repeats the input sequence; (cycle [1 2 3]) returns a lazy sequence of (1 2 3 1 2 3 1 2...). So now that the row continues forever, we can apply nth to grab the cell we want.

Of note is the use of the thread-first macro ->, instead of the thread-last macro ->> I've used extensively. This runs each expression in order, feeding the result of each expression as the first parameter of the next expression, rather than sending it as the last parameter like thread-last. In general, operations on collections (map, reduce, filter) tend to use ->> because the collection argument is last, while operations on single values tend to use ->.

(defn value-at [tree-lines row col]
  (-> (tree-lines row)
      cycle
      (nth col)))

Now the solve function is pretty simple. We split the input into a list of lines, then find all of the target coordinates within the map. We then map each coordinate to its value on the map, filter out only the values that are trees, and count up the result.

(defn solve [input path]
  (let [tree-lines (str/split-lines input)]
    (->> (target-coordinates path (count tree-lines))
         (map (fn [[x y]] (value-at tree-lines y x)))
         (filter tree?)
         count)))

And the final part1 function just uses the solve function with the given input and the path of [x=3 y=1].

(def part1-path [3 1])
(defn part1 [input]
  (solve input part1-path))

Part 2

Part two uses the same basic logic as part 1, except that we now want to run through the map multiple times, using several paths. We look at the number of trees in each path, and then multiply the results together. Given the construction of the solve function accepting the path to run, this is simple.

All we need to do is take each path (list of [x y] coordinates), map each to the number of trees via the solve function, and use (apply *) to multiply the results. Note again that we use thread-last (->>) since both map and apply operate on collections.

(def part2-paths [[1 1] [3 1] [5 1] [7 1] [1 2]])

(defn part2 [input]
  (->> part2-paths
       (map (partial solve input))
       (apply *)))

Happy tobogganing!