img of Advent of Code 2024 - Day 8: Resonant Collinearity

Advent of Code 2024 - Day 8: Resonant Collinearity


Attention!

  • Check Advent of Code for the full challenge details. They created everything we need to know about the challenge - give them your support.

  • My solutions are not ‘the’ solutions - I target one of the possible solutions. They might not be the most efficient, but they are a way to solve the problem. I do not plan to spend too much time the challenges.

  • Every challenge = different language - just for fun and learning.

  • I do not include the input data, but you can find it on the Advent of Code website or in the repository of the code.


For today’s Advent of Code challenge, I’ve been tasked with solving a problem involving antennas emitting signals and creating antinodes based on their frequencies. The problem requires identifying unique locations where resonant collinearity occurs. Here’s how I approached the challenge.

Problem Breakdown

Part One: Counting Antinodes

I was given a map of antennas represented by a grid, with each antenna being marked by a specific character (either a letter or a digit). The key task here was to identify all the locations where “antinodes” occur. An antinode is formed when two antennas of the same frequency are perfectly in line, with antinode being twice as far away as antennas were spaced apart. The goal in Part One was to calculate how many unique locations on the map contain at least one antinode.

Part Two: Adjusting for Resonant Harmonics

For Part Two, the problem becomes a bit more complex. Now, I need to consider all locations where two antennas of the same frequency are in line, regardless of the distance between them. This means that antinodes now also occur at the positions of the antennas themselves if they are in line with at least one other antenna of the same frequency. The task in Part Two is to calculate the number of unique antinode locations, including these new ones.

Solution Approach

I used Kotlin to implement the solution for today’s task.

Step 1: Parsing the Input

First, I read the input from a text file, which contains the grid representing the antennas. I then looped through the grid, creating a Vec2 data structure for each antenna’s position (representing coordinates) and storing them in a map. This map associates each antenna’s frequency (represented by a character) with a set of positions on the grid.

// main.kt
val lines = File("input.txt").readLines()
val antennas = mutableMapOf<Char, MutableSet<Vec2>>()

for (row in lines.indices)
for (col in lines[0].indices) {
    val cell = lines[row][col]
    if (cell == '.') continue;
    antennas.computeIfAbsent(cell) { mutableSetOf() }
        .add(Vec2(row, col))
}

Not at first, but after refactoring I expanded operators for Vec2. Normally, I do not like operator overloading but if it’s ‘just for vectors math’ -> it could be already built in.

data class Vec2(val x: Int, val y: Int) {
    operator fun minus(other: Vec2) = Vec2(this.x - other.x, this.y - other.y)
    operator fun plus(other: Vec2) = Vec2(this.x + other.x, this.y + other.y)
    operator fun times(scalar: Int) = Vec2(this.x * scalar, this.y * scalar)
}

I created a helper function inBounds to check if a given coordinate is within the grid boundaries.

// main.kt
val linesYRange = lines.indices
val linesXRange = lines[0].indices
val inBounds = { vec: Vec2 -> vec.x in linesXRange && vec.y in linesYRange }

Step 2: Finding Antinodes (Part One)

For Part One, I needed to find all positions where an antinode could exist. I checked every pair of antennas of the same frequency and calculated their relative positions. From there, I computed the positions of potential antinodes based on the distance between them, specifically looking for positions where the distance was doubled. These positions were then added to a set of antinodes.

// main.kt
val antinodes1 = mutableSetOf<Vec2>()
// val antinodes2 = mutableSetOf<Vec2>() - for part two

for (antenna in antennas.keys)
for (v1 in antennas[antenna]!!)
for (v2 in antennas[antenna]!!) {
    if (v1 == v2) continue
    val distance = v2 - v1
    // relative distance from doubled distance
    val antinode = v1 + (distance * 2) 
    if (inBounds(antinode)) {
        antinodes1.add(antinode)
    }
}

Step 3: Adding Resonant Harmonics (Part Two)

For Part Two, the main change was that I needed to track every antinode along the line between antennas, not just those formed by double the distance. I added a loop that iterates along the line between each pair of antennas, marking every position along the way as an antinode. It was possible to just plug it into the same loops after antinodes for partOne.

// main.kt
var positiveNextLocation = v1
var negativeNextLocation = v1
do {
    positiveNextLocation = positiveNextLocation + distance
    negativeNextLocation = negativeNextLocation - distance
    if (inBounds(negativeNextLocation)) {
        antinodes2.add(negativeNextLocation)
    }
    if (inBounds(positiveNextLocation)) {
        antinodes2.add(positiveNextLocation)
    }
} while (inBounds(positiveNextLocation))

Step 4: Output the Results

Finally, the final result is just the size of antinodes set, for both cases.

// main.kt
println(antinodes1.size)  // Part One
println(antinodes2.size)  // Part Two

Final Solution Code

The final solution code can be found on my GitHub repository.

If you like the content, don’t forget to give it a star ⭐.

Time and Space Complexity

  • Time Complexity: O(A^2 * N), where A is the number of unique antenna frequencies and N is the average number of antennas per frequency. I iterate over every possible pair of antennas, and for each pair, I calculate potential antinode positions.
  • Space Complexity: O(A * N), as I store the positions of antennas and antinodes in sets.

Conclusion

This challenge required me to approach a problem involving coordinate geometry and set operations. The code is not that hard, I was just important to remember from school how to deal with simple vectors.

Part One involves calculating the number of unique antinode locations considering only those formed by doubled distances between antennas, while Part Two expands the problem by including every point along the line between antennas, marking those as antinodes.

My code was working but it looked very poorly at first. I looked up quite a few kotlin examples and learned that operator overloading is a thing in Kotlin… and that was a GOOD thing.

This was a fun problem, and I think Kotlin is a fun (pun intended) language.

Tommorow I will use Python.