Andrey Listopadov

Advent of Code: Day 7

@aoc2021 @programming clojure ~3 minutes read

Remembering last year’s AoC, I can tell that solving one puzzle per day is much more pleasant than solving a bunch of puzzles in a single day. Last year I felt the burden of unsolved days, and an eternal race with the clock to keep up with others. Thankfully, this year is different!

The Treachery of Whales

Today a giant whale wants to eat us, but a swarm of crabs in tiny submarines wants to help us escape. In order to do that, we need to help crabs to align in a straight line with the least amount of fuel. The example input resembles a horizontal position of each crab, and goes as follows: 16,1,2,0,4,2,7,1,2,14.

When crab moves from position 0 to position 1 its submarine looses 1 unit of fuel. So to move from position 16 to 5, the crab needs to spend 11 units of fuel. We need to find such a position that total fuel unit loss is as small as possible. Let’s start with the example input:

day6> (ns day7)
nil
day7> (require '[aoc-commons :refer [parse-long]]
               '[clojure.string :as str])
nil
day7> (def example-input [16,1,2,0,4,2,7,1,2,14])
#'day7/example-input

Now we just need to compute fuel consumption for a given position:

day7> (defn fuel-consumption [input pos]
        (->> input
             (map #(Math/abs (- % pos)))
             (reduce +)))
#'day7/fuel-consumption
day7> (fuel-consumption example-input 2)
37

As can be seen, computing fuel consumption for position 2 yields a correct result. Now we only need to check what position is the most optimal one, meaning that it uses the least amount of fuel:

day7> (defn solve [input]
        (let [min (apply min input)
              max (apply max input)]
          (->> (range min (inc max))
               (map (partial fuel-consumption input))
               sort
               first)))
#'day7/solve
day7> (solve example-input)
37

We only care for the positions in range of our numbers. So if the minimum position is 10 and the maximum is 15 there’s no point going below 10 or above 15. Since the range yields a non-inclusive range, we need to inc the upper bound.

This again yields 37, as it is the least amount of fuel we can use to align everyone. It also works for the real input, but it is quite slow:

day7> (time (solve (read-input)))
"Elapsed time: 6545.965142 msecs"
343468

While slowness may be an issue in the second part, as the previous day clearly illustrated, let’s hope it will not cause too much trouble.

Part two

OK, we’ve got it slightly wrong, and crab submarines use a lot more fuel to move. So to move from position 1 to 5, the submarine uses 1+2+3+4+5 amount of fuel. This is called a triangular number, and is computed with the following formula: n(n+1)/2:

day7> (defn triangle-number [n]
        (/ (* n (inc n)) 2))
#'day7/triangle-number
day7> (triangle-number 5)
15

Now, all we need to do is to plug this function into our solution. Instead of duplicating the code, let’s rewrite our two functions to support an additional modifier argument:

day7> (defn fuel-consumption [input modifier pos]
        (->> input
             (map #(modifier (Math/abs (- % pos))))
             (reduce +)))
#'day7/fuel-consumption
day7> (defn solve
        ([input] (solve input identity))
        ([input modifier]
         (let [min (apply min input)
               max (apply max input)]
           (->> (range min max)
                (map (partial fuel-consumption input modifier))
                sort
                first))))
#'day7/solve

The solve function now optionally accepts a second argument. When only one argument is given, the arity dispatcher goes to the first body ([input] (solve input identity)), which calls the same function but with two arguments. The default value for modifier is identity, which is a function that returns the argument it was given as is. Now we can use our triangle-number as a modifier to compute the result:

day7> (solve example-input)
37
day7> (solve example-input triangle-number)
168

This works for real input data as well:

day7> (time (solve (read-input) triangle-number))
"Elapsed time: 6256.100167 msecs"
96086265

The required time hasn’t changed much, as we just added one small formula to an already working solution. I’m fine with the current results.

Day 7 thoughts

Today’s task was quite simple. Perhaps there’s a more optimal solution to it, but I can’t think of one to be honest. Luckily, today the speed was not a blocker, just a minor inconvenience.

See you tomorrow!