Advent of Code 2023 - Day 06

By Eric Burden | December 6, 2023

It’s that time of year again! Just like last year, I’ll be posting my solutions to the Advent of Code puzzles. This year, I’ll be solving the puzzles in Kotlin. I’ll post my solutions and code to GitHub as well. If you haven’t given AoC a try, I encourage you to do so along with me!

Day 6 - Wait For It

Find the problem description HERE.

The Input - Hold, Hold, Hold Your Boat

Today’s input is really short. Two lines, with whitespace-separated numbers being all we care about. I did try to be a bit fancy with it, but it’s still very straightforward. I did make use of the ability to assign an anonymous function to a variable, which is new for me in Kotlin. A welcome break from yesterday!

/**
 * Class that represents one of the current distance records
 *
 * Each race can be represented as the time limit and the last record distance.
 *
 * @property time The time available to complete the race.
 * @property distance The last record distance (we need to beat).
 */
data class RaceRecord(val time: Double, val distance: Double) {
    companion object {
        /**
         * Parse the input from a string representing the input file
         *
         * Today's input is only two lines and they both mean different things, so we'll parse the
         * input as a single string, convert each line to a list of numbers, and zip them together
         * to make our [RaceRecord]s
         */
        fun parseInput(input: String): List<RaceRecord> {
            val toDoubleOrThrow: (String) -> Double = { n ->
                n.toDoubleOrNull()
                        ?: throw IllegalArgumentException("$n cannot be parsed to a Double!")
            }

            val (raceTimes, raceDistances) =
                    input.trim().split("\\n".toRegex()).map { line ->
                        line.replaceFirst("\\w+:".toRegex(), "")
                                .trim()
                                .split("\\s+".toRegex())
                                .map(toDoubleOrThrow)
                    }

            return raceTimes.zip(raceDistances) { time, distance -> RaceRecord(time, distance) }
        }
    }
}

class Day06(input: String) {

    // One string to one list. Piece of cake.
    private val parsed = RaceRecord.parseInput(input)

}

We’ve seen a pattern of alternating difficulty levels on puzzles so far this year, and today seems to be no exception.

Part One - We’re Going Quadratic!

Do you ever get that feeling when you’re reading a problem statement that there’s probably some easy way to solve it if you just used math? Sometimes I get that feeling, and it really grinds my gears because I can’t figure out what the math is. And sometimes, the math reveals itself. Today, the math came through! Here in part one, we need to figure out how many ways we can win a boat race based on how long we need to charge up the boat.


// My first imports!
import kotlin.math.ceil
import kotlin.math.floor
import kotlin.math.sqrt

data class RaceRecord(val time: Double, val distance: Double) {
    // companion object { ... }

    /**
     * Return the number of different ways we can win the race!
     *
     * @ return An Int indicating just how many different hold times will get us
     * the winning distance.
     */
    fun countWinningStrategies(): Int {
        // For: Hold Time -> h; Race Time -> t; Distance to Beat -> d
        // We can derive a formula for beating the previous record as:
        //   - (t - h) * h > d
        //
        // Which rearranges to: (-1)*h^2 + (t * h) - d > 0
        // Now, that there is a quadratic expression! You know, the old
        // `ax^2 + bx + c = 0`? We can solve it like:
        //     h = (-t +/- sqrt(t^2 - 4 * (-1) * (-d))) / 2 * (-1)
        //
        // That's math! The 'tricky' part is that we don't want to know the _exact_
        // hold time to _equal_ the previous record, we want to know the smallest
        // and largest hold times that will _beat_ the record. For that, we round
        // _away_ from the mean possible hold time and then adjust our value towards
        // the mean by 1. For example, the results of this formula for the first
        // example race (7ms, 9mm) indicate that you could travel the 9mm by holding
        // the button for 1.697ms or 5.303ms. The minimum number of whole milliseconds,
        // then is `floor(1.697) + 1` and the maximum is `ceil(5.303) - 1`. This
        // correctly handles cases where the exact time is an integer as well.
        val sqrtFormulaPart = sqrt((time * time) - (4 * distance))
        val minWinningHold = floor((-time + sqrtFormulaPart) / -2).toInt() + 1
        val maxWinningHold = ceil((-time - sqrtFormulaPart) / -2).toInt() - 1

        // The total number of winning holds is the length of the inclusive range
        // of all possible winning holds.
        return (maxWinningHold - minWinningHold) + 1
    }
}

class Day06(input: String) {

    // One string to one list. Piece of cake.
    // private val parsed = ...

    // In part 1, we calculate all our winning hold times and return
    // the product of that count from each race.
    fun solvePart1(): Int = parsed
      .map { it.countWinningStrategies() }
      .reduce { acc, n -> acc * n }

}

Yay, math!

Part Two - Kerning for Dummies

You know, one of these days, we’re going to stop and take a second look at an input before we start writing code. Of course, that would take half the fun out of solving these puzzles, so there’s that. Turns out, there’s only one race, so we just need to figure out how many ways we can win that one.


data class RaceRecord(val time: Double, val distance: Double) {
    // companion object { ... }

    // fun countWinningStrategies(): Int { ... }
}

class Day06(input: String) {

    // One string to one list. Piece of cake.
    // private val parsed = ...

    // In part 1, we calculate all our winning hold times and return
    // the product of that count from each race.
    // fun solvePart1(): Int = ...

    // In part 2, we learn what 'kerning' is and get mad about it. Then
    // we concatenate all the race times/records into one and figure how
    // how many ways we can win that one race.
    fun solvePart2(): Int {
        val uberRaceTime = parsed
          .joinToString("") { it.time.toInt().toString() }
          .toDouble()
        val uberDistance = parsed
          .joinToString("") { it.distance.toInt().toString() }
          .toDouble()
        return RaceRecord(uberRaceTime, uberDistance).countWinningStrategies()
    }
}

Yep, the same math works for part 2!

Wrap Up

Well, well, well, here we are again with a lighter day. I don’t have too much to say about today’s puzzle, probably because I’m still recovering from yesterday. I do appreciate the opportunity to get caught back up, though, which is nice. Let’s all enjoy this lighter day, and get ready for the fresh trial that the next odd-numbered day is likely to bring!

comments powered by Disqus