DEV Community

Cover image for Resonant Collinearity
Robert Mion
Robert Mion

Posted on

Resonant Collinearity

Advent of Code 2024 Day 8

Part 1

I think I get it. Can I do it?

As I understand it:

  • For each pair of identical ligatures
  • Find the point X where one ligature is 1N and the other is 2N - where N is some distance from X
  • As long as it is within the bounds of the grid, count it toward the answer

Here's a visual:

...........
...........
...X.......
...........
.....Y.....   <---1N from top X, 2N from bottom X
...........
.......Y...   <---2N from top X, 1N from bottom X
...........
.........X.
...........
Enter fullscreen mode Exit fullscreen mode

It's clear visually.

How could I determine it algorithmically?

Calculating 1N and 2N algorithmically

Here's the example grid:

............
........0...
.....0......
.......0....
....0.......
......A.....
............
............
........A...
.........A..
............
............
Enter fullscreen mode Exit fullscreen mode

I'll focus on the 0s:

  • There are four
  • Coordinates are: 1,8, 2,5, 3,7, 4,4

Comparing the first two:

1,8 vs. 2,5

1 row apart
3 col apart

2 possible antinodes:
0,11: 0 = min(1,2) - 1
3,2

For 0,11
0 = min(1,2) - 1
11 = ....
Enter fullscreen mode Exit fullscreen mode

I realized while writing that I really need to know the slope of the line connecting the pair of points.

That way I can know whether to add or subtract from each axis to determine where the antinode is.

Refreshing my memory about slope

The formula is:

(y2 - y1) / (x2 - x1)
Enter fullscreen mode Exit fullscreen mode

The outcome will be one of these four:

  • > 0 means positive slope: up and to the right
  • < 0 means negative slope: down and to the right
  • 0 means horizontal line
  • Infinity means vertical line

Returning to the example:

1,8 and 2,5

(5 - 8) / (2 - 1) = -3 / 1 = -3
Enter fullscreen mode Exit fullscreen mode

What? A negative slope? No, that line has positive slope!

Oh...I see.

Array indexing counts up, but visually is moving down.

I need to count indices in reverse.

Instead of like this:

0 ............
1 ........0...
2 .....0......
3 .......0....
4 ....0.......
5 ......A.....
6 ............
7 ............
8 ........A...
9 .........A..
  ............
  ............
  0123456789
Enter fullscreen mode Exit fullscreen mode

I need to count like this:

  ............
  ........0...
9 .....0......
8 .......0....
7 ....0.......
6 ......A.....
5 ............
4 ............
3 ........A...
2 .........A..
1 ............
0 ............
  0123456789
Enter fullscreen mode Exit fullscreen mode

It should just require a bit more math:

array length - current row/col index
Enter fullscreen mode Exit fullscreen mode

I'll try.

For the top-most 0:

12 rows
Row index: 1
12 - 1 = 11

Column index: 8

Coordinates: 8,11
Enter fullscreen mode Exit fullscreen mode

For the 0 in the next row:

Row index: 2
12 - 2 = 10

Column index: 5

Coordinates: 5,10
Enter fullscreen mode Exit fullscreen mode

And the slope:

(10 - 11) / (5 - 8)
   -1     /    -3
         1/3
Enter fullscreen mode Exit fullscreen mode

A positive slope! That's correct!

Starting to code

Building the list of antenna coordinates

An empty object filled with a nested for loop:

let graph = input.split('\n').map(el => el.split(''))
let antennas = {}
for (let y = 0; y < graph.length; y++) {
  for (let x = 0; x < graph[0].length; x++) {
    if (graph[y][x] !== '.') {
      if (!(graph[y][x] in antennas)) {
        antennas[graph[y][x]] = []
      }
      antennas[graph[y][x]].push([graph.length - y,x])
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Creates this object for the example input:

{
  '0': [ [ 11, 8 ], [ 10, 5 ], [ 9, 7 ], [ 8, 4 ] ],
  A: [ [ 7, 6 ], [ 4, 8 ], [ 3, 9 ] ]
}
Enter fullscreen mode Exit fullscreen mode

Looks great!

Next, calculating slope.

Writing an overly complex antinode-finder

My scope function is simple:

function getScope(p1,p2) {
  return (p2[0] - p1[0]) / (p2[1] - p1[1])
}
Enter fullscreen mode Exit fullscreen mode

It expects two arrays and returns the scope using all four coordinates.

I'll want to call this function when comparing all pairs of similar-frequency coordinates.

That comparison happens in this super-nested for loop:

for (let freq in antennas) {
  let f = antennas[freq]
  for (let i = 0; i < f.length; i++) {
    for (let j = i + 1; j < f.length; j++) {
      // Comparing two antennas of the same frequency
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Confirming it works on the example input:

[ 11, 8 ] [ 10, 5 ]
[ 11, 8 ] [ 9, 7 ]
[ 11, 8 ] [ 8, 4 ]
[ 10, 5 ] [ 9, 7 ]
[ 10, 5 ] [ 8, 4 ]
[ 9, 7 ] [ 8, 4 ]
[ 7, 6 ] [ 4, 8 ]
[ 7, 6 ] [ 3, 9 ]
[ 4, 8 ] [ 3, 9 ]
Enter fullscreen mode Exit fullscreen mode

Nine comparisons. That's right!

And the scopes for each?

Those look right, too, thankfully.

Now for the overly complicated part, I think.

Handling all four slope types

They are:

- 0
\ -1
| Infinity
/ +1
Enter fullscreen mode Exit fullscreen mode

Let's handle this.

It's a lot, but the subtleties are important within each clause:

let slope = getScope(f[i], f[j])
if (slope == Infinity) { 
  let Ydiff = Math.abs(f[i][0] - f[j][0])
  let node1 = [Math.max(f[i][0],f[j][0]) + Ydiff, f[i][1]]
  let node2 = [Math.min(f[i][0],f[j][0]) - Ydiff, f[i][1]]
} else if (slope < 0) {
  let Ydiff = Math.abs(f[i][0] - f[j][0])
  let Xdiff = Math.abs(f[i][1] - f[j][1])
  let node1 = [Math.min(f[i][0],f[j][0]) - Ydiff, Math.max(f[i][1],f[j][1]) + Xdiff]
  let node2 = [Math.max(f[i][0],f[j][0]) + Ydiff, Math.min(f[i][1],f[j][1]) - Xdiff]
} else if (slope == 0) {
  let Xdiff = Math.abs(f[i][1] - f[j][1])
  let node1 = [f[i][0], Math.max(f[i][1],f[j][1]) + Xdiff]
  let node2 = [f[i][0], Math.min(f[i][1],f[j][1]) - Xdiff]
} else if (slope > 0) {
  let Ydiff = Math.abs(f[i][0] - f[j][0])
  let Xdiff = Math.abs(f[i][1] - f[j][1])
  let node1 = [Math.max(f[i][0],f[j][0]) + Ydiff, Math.max(f[i][1],f[j][1]) + Xdiff]
  let node2 = [Math.min(f[i][0],f[j][0]) - Ydiff, Math.min(f[i][1],f[j][1]) - Xdiff]
}
Enter fullscreen mode Exit fullscreen mode

Thankfully, all identified antinodes seem correctly placed.

Next, excluding ones that are out-of-bounds

Excluding the out-of-bounds antinodes

Enter...more conditions!

function addNode(p1) {
  if (
    p1[0] >= 0 && p1[0] < graph.length &&
    p1[1] >= 0 && p1[1] < graph[0].length
  ) {
    valid.add(p1.join(','))
  }
}
Enter fullscreen mode Exit fullscreen mode

I am checking for whether each coordinate is between 0 and the length of the rows or the columns.

Then, at the bottom of each clause in my antinode-finder, I call the function on both possible nodes:

addNode(node1)
addNode(node2)
Enter fullscreen mode Exit fullscreen mode

And my answer will be the size of my valid set.

Does this actually work?

Running it generates 12. Not 14.

Why not?

...

After some debugging, I found my error:

antennas[graph[y][x]].push([graph.length - y,x])
Enter fullscreen mode Exit fullscreen mode

I'm off by one in my assignment of the row. I need a value that is one less:

antennas[graph[y][x]].push([graph.length - 1 - y,x])
Enter fullscreen mode Exit fullscreen mode

That fixes things.

I see 14 now.

Time to run it on my puzzle input.

...

Correct answer!!!

That took a lot longer than I expected, but I did it!

I can only imagine what Part 2 will require.

Gulp.

Part 2

Yup. Lots more antinodes to identify.

This feels harder, though it may be a relatively simple adjustment.

Time to think on it.

...

Going in with low confidence of generating the correct answer

Mostly because of this gotcha:

exactly in line with at least two antennas of the same frequency

I think I understand this criteria.

My hunch is that at long as there are three of any given frequency, all three antennas are also antinodes.

If I'm wrong, then that's likely why my answer will be off: mistaking an antenna for an antinode.

But I think I have a strategy for identifying all the new antinodes.

Enter: more while loops to walk the lines

My current algorithm finds the antinodes on either end of two antennas.

I instead want to walk along the line in both directions until I am about to step out of bounds.

This will require some refactoring.

I'm ready.

Here's my updated condition for positive slope lines:

if (slope > 0) {
  let Ydiff = Math.abs(f[i][0] - f[j][0])
  let Xdiff = Math.abs(f[i][1] - f[j][1])
  let next1 = [Math.max(f[i][0],f[j][0]) + Ydiff, Math.max(f[i][1],f[j][1]) + Xdiff]
  while (isInBounds(next1)) {
    valid.add(next1.join(','))
    next1 = [next1[0] + Ydiff, next1[1] + Xdiff]
  }
  let next2 = [Math.min(f[i][0],f[j][0]) - Ydiff, Math.min(f[i][1],f[j][1]) - Xdiff]
  while (isInBounds(next2)) {
    valid.add(next2.join(','))
    next2 = [next2[0] - Ydiff, next2[1] - Xdiff]
  }
}
Enter fullscreen mode Exit fullscreen mode

What changed:

  • I perform Math once up front
  • Inside the while loop I add the coordinate, then I just increment or decrement each coordinate by the corresponding diff
  • The condition uses my updated function which returns a boolean instead of adding the coordinate automatically

I had to do this for each clause.

I messed one up slightly, which caused me to get an off-by-1 answer using the example input, and to see a really weird grid, which helped me diagnose which clause was malfunctioning.

Eventually, I got it to work on the example input.

Then I ran it on my puzzle input.

And...

I generated the correct answer!!!

I'm so proud of myself!

And I'm so grateful that there was no sneaky edge case in my puzzle input!

Wow, that took several days of passive thinking to work through.

On to another day with two hard earned gold stars.

Top comments (0)