Skip to Content

Advent of Code 2018 - Day 6, in Kotlin

Kotlin solutions to parts 1 and 2 of Advent of Code 2018, Day 6: 'Chronal Coordinates'

Posted on

Day 6 is here, and with it our first grid problem. Advent of Code usually contains a couple, so this is your opportunity to learn all about Manhattan Distance.

If you’d rather just view code, the GitHub Repository is here.

Side Note: Today’s code and writeup is way later than usual because I was speaking (about Kotlin!) at the Lead Developer Austin conference. I had a great time presenting and learned so much from the other speakers. It is a very well run conference. If you have an opportunity to attend one of the Lead Developer conferences, I suggest you take the opportunity to do so.

Problem Input

Our input is a list of Strings, which are easily parsed, so I don’t do anything fancy. We will define a data class to hold the input below.

Day 6, Part 1

The device on your wrist beeps several times, and once again you feel like you’re falling.

“Situation critical,” the device announces. “Destination indeterminate. Chronal interference detected. Please specify new target coordinates.”

The device then produces a list of coordinates (your puzzle input). Are they places it thinks are safe or dangerous? It recommends you check manual page 729. The Elves did not give you a manual.

If they’re dangerous, maybe you can minimize the danger by finding the coordinate that gives the largest distance from the other points.

Using only the Manhattan distance, determine the area around each coordinate by counting the number of integer X,Y locations that are closest to that coordinate (and aren’t tied in distance to any other coordinate).

Your goal is to find the size of the largest area that isn’t infinite. For example, consider the following list of coordinates:

1, 1
1, 6
8, 3
3, 4
5, 5
8, 9

If we name these coordinates A through F, we can draw them on a grid, putting 0,0 at the top left:

..........
.A........
..........
........C.
...D......
.....E....
.B........
..........
..........
........F.

This view is partial - the actual grid extends infinitely in all directions. Using the Manhattan distance, each location’s closest coordinate can be determined, shown here in lowercase:

aaaaa.cccc
aAaaa.cccc
aaaddecccc
aadddeccCc
..dDdeeccc
bb.deEeecc
bBb.eeee..
bbb.eeefff
bbb.eeffff
bbb.ffffFf

Locations shown as . are equally far from two or more coordinates, and so they don’t count as being closest to any.

In this example, the areas of coordinates A, B, C, and F are infinite - while not shown here, their areas extend forever outside the visible grid. However, the areas of coordinates D and E are finite: D is closest to 9 locations, and E is closest to 17 (both including the coordinate’s location itself). Therefore, in this example, the size of the largest area is 17.

What is the size of the largest area that isn’t infinite?

First, let’s create a Point data class before we do anything else. We’ll probably need it later, so let’s define it in a new place, not associated with this specific challenge. We’ll include the parsing logic for it, hopefully that will be OK going forward, but we can always refactor.

data class Point(val x: Int, val y: Int) {
    fun distanceTo(otherX: Int, otherY: Int): Int =
        abs(x - otherX) + abs(y - otherY)

    companion object {
        fun of(input: String): Point =
            input.split(",")
                .map { it.trim().toInt() }
                .run { Point(this[0], this[1]) }
    }
}

It’s pretty simple - it has an x and y coordinate, and can determine the distance to another x and y pair. There is no version of distanceTo that takes another Point because we don’t actually need it yet. When we do, we’ll add it later. Our parse function splits our string on comma and turns the resulting String objects into Ints. From there we use run as a map function to turn our array of Ints into a Point.

Now let’s look at the challenge. If you think about it, once we have a List<Point> we’ll need to know where the bounds are. Meaning: the largest and smallest x values, and the largest and smallest y values make up a bounding box around our area. We’ll define those as IntRange objects, because that seems natural:

class Day06(input: List<String>) {

    private val points: List<Point> = input.map { Point.of(it) }
    private val xRange: IntRange = (points.minBy { it.x }!!.x..points.maxBy { it.x }!!.x)
    private val yRange: IntRange = (points.minBy { it.y }!!.y..points.maxBy { it.y }!!.y)

}

The next thing we need to think about is how to identify the Points that are in infinite areas. In principle, if we are on the edge of a bounding box, the spot closest to us is infinite. So let’s write a function to determine if a Point is infinite.

private fun isEdge(x: Int, y: Int): Boolean =
    x == xRange.first || x == xRange.last || y == yRange.first || y == yRange.last

Now that we can do all that, let me show you the solution, and then explain it.

fun solvePart1(): Int {
    val infinite: MutableSet<Point> = mutableSetOf()
    return xRange.asSequence().flatMap { x ->
        yRange.asSequence().map { y ->
            val closest = points.map { it to it.distanceTo(x, y) }.sortedBy { it.second }.take(2)
            if (isEdge(x, y)) {
                infinite.add(closest[0].first)
            }
            closest[0].first.takeUnless { closest[0].second == closest[1].second }
        }
    }
        .filterNot { it in infinite }
        .groupingBy { it }
        .eachCount()
        .maxBy { it.value }!!
        .value
}

Put the pitchforks down, I’ll explain!

First, we set up a set for all of the Points we discover are infinite. Meaning, those points that are close to an edge. Then we set up a familiar flatmap/map loop over our x and y ranges. This gives us a sequence of x/y pairs representing every spot in our grid. For each of those, we calculate the distance to every Point, and take the first two of them. Why two? Because we need to account for places in the grid that are equidistant to two Points. We calculate if the current x/y pair is on the edge, and if so put the closest matching Point into the infinite set.

At this point we take the closest point, only if its distance is not equal to the second closest Point. This uses one of my favorite functions - takeUnless. There is a takeIf as well, which does the opposite. At this point, we have a List<Point?>. By filtering out Point objects that are in infinite, we correctly handle the case where we can’t actually look at infinite spots, as well as filtering out nulls because null is not in the set of infinites (I never took philosophy, but this seems like a question you could really think about for a while).

Once we have that, group and count the Points, and find the one with the highest count, returning it.

And that is one hard earned star! Onward!

Day 6, Part 2

On the other hand, if the coordinates are safe, maybe the best you can do is try to find a region near as many coordinates as possible.

For example, suppose you want the sum of the Manhattan distance to all of the coordinates to be less than 32. For each location, add up the distances to all of the given coordinates; if the total of those distances is less than 32, that location is within the desired region. Using the same coordinates as above, the resulting region looks like this:

..........
.A........
..........
...###..C.
..#D###...
..###E#...
.B.###....
..........
..........
........F.

In particular, consider the highlighted location 4,3 located at the top middle of the region. Its calculation is as follows, where abs() is the absolute value function:

  • Distance to coordinate A: abs(4-1) + abs(3-1) = 5
  • Distance to coordinate B: abs(4-1) + abs(3-6) = 6
  • Distance to coordinate C: abs(4-8) + abs(3-3) = 4
  • Distance to coordinate D: abs(4-3) + abs(3-4) = 2
  • Distance to coordinate E: abs(4-5) + abs(3-5) = 3
  • Distance to coordinate F: abs(4-8) + abs(3-9) = 10

Total distance: 5 + 6 + 4 + 2 + 3 + 10 = 30

Because the total distance to all coordinates (30) is less than 32, the location is within the region.

This region, which also includes coordinates D and E, has a total size of 16.

Your actual region will need to be much larger than this example, though, instead including all locations with a total distance of less than 10000.

What is the size of the region containing all locations which have a total distance to all given coordinates of less than 10000?

Don’t worry, this isn’t as hard as it sounds. It took me a bit to understand it. Essentially it says “if you took every spot in the grid, and calculated its distance to every Point, how many of them have a total distance of less than 10,000?” That doesn’t seem so bad, let’s code it up!

fun solvePart2(range: Int = 10000): Int =
    xRange.asSequence().flatMap { x ->
        yRange.asSequence().map { y ->
            points.map { it.distanceTo(x, y) }.sum()
        }
    }
        .filter { it < range }
        .count()

As you can see, range is an optional argument defaulting to 10,000 so we can unit test it with 32 against the sample input. Next, we do the flatmap/map thing again, and for each point we sum up the distance to all of the Points. If we filter out anything whose distance is greater than the range, we can count how many results we have for our answer!

Second star earned! I can’t wait for tomorrow!

Further Reading

  1. Index of All Solutions - All solutions for 2018, in Kotlin.
  2. My Github repo - Solutions and tests for each day.
  3. Solution - Full code for day 6
  4. Advent of Code - Come join in and do these challenges yourself!