DEV Community

Cover image for Never Tell Me The Odds
Robert Mion
Robert Mion

Posted on

Never Tell Me The Odds

Advent of Code 2023 Day 24

Part 1

Curse you, Linear Algebra!

I read through the puzzle description.

I saw that it involves:

  • Lines existing in 3D - though for now, we care only about 2 of the D's
  • Finding the possible intersection between any two lines
  • Checking whether the X,Y coordinates of that intersection also intersect with an arbitrarily-sized bounding box

Then I saw the floating point numbers where two lines could meet.

And I saw the bounding box. And all the zeroes!

And I saw the X,Y,Z values of the lines in my puzzle input.

And it's a lot.

A lot of math that I don't understand.

But sometimes puzzles like this are a great excuse to do some research.

And find new formulas.

And new libraries.

Sure, some might consider that cheating.

But I consider it part of solving the puzzle.

I might use someone else's algorithm to compute a critical step in search of the correct answer.

As long as I don't use someone else's algorithm to specifically generate the correct answer.

Discovering new formulas and libraries

I'm given:

  • The coordinates of a point
  • The parameters by which that point moves

Using that second data point, I can calculate an infinite number of other points on the straight line.

I only need one other point...

...to calculate the line's slope:

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

Cool, I can figure out each line's slope.

But then what?

I really want to know if two lines intersect, and where.

A google search for javascript two lines intersect generates some very useful results:

I read the relevant section ini Paul's article.

I didn't understand it, sadly.

But I do see how the JavaScript code perfectly maps Paul's formula into JavaScript syntax.

So, I'll call that a win.

Now I want to call this function with some points from the example input to confirm it works the way I need it to.

Testing Leo's function of Paul's formula

The intersect function expects eight parameters representing four pair of X,Y coordinates from two lines.

I provide it points from the first two hailstones in the example input.

I get false.

That's not what I wanted.

Why did I get false?

Well, there are three conditions in the function that could generate a false return value:

  • the lines are of length 0
  • Lines are parallel
  • the intersection is along the segments

I am triggering that last one.

What if I remove that condition? Especially since it seems like a fact I don't care to check for in this challenge.

I now get the X and Y values I was expecting.

Good!

Now I want to test two points that crossed in the past to confirm I understand what that means.

I assume it means their intersection occurs on the line away from where the velocity is heading.

Values updated.

Function ran.

And just as the description stated, the X,Y coordinates are for a point on Hailstone A's line that is in the opposite direction of where it's headed.

It looks like this function suits my needs for Part 1.

But I still want to test math.js's Function Intersect method.

I feel like I'll need it for Part 2 where I'm likely to use the Z portion of each coordinate.

Testing math.js's Function Intersect

I installed the npm package in my replit.com sandbox.

I replaced my eight values with four 2-element arrays for each pair.

I ran the code.

It took a few seconds to complete, surprisingly.

But it spit out the same value and Leo's function.

I wonder why it took a few seconds to run?

For now, I think I'll stick with Leo's function.

Parsing the input into two endpoints

For a line like this:

19, 13, 30 @ -2,  1, -2
Enter fullscreen mode Exit fullscreen mode

I need to use four of the six numbers:

 A,  B,  C @  D,  E,  F
 *   *        *   *
Enter fullscreen mode Exit fullscreen mode

And do two calculations:

A + D
B + E
Enter fullscreen mode Exit fullscreen mode

To get four final numbers:

X1 = A
Y1 = B
X2 = A + D
Y2 = B + E
Enter fullscreen mode Exit fullscreen mode

I'll use regex to get all six numbers:

/(\d+)/g
Enter fullscreen mode Exit fullscreen mode

Here's the beginning of my reduce algorithm:

const part1 = input.split('\n').reduce(
  (total, stone, index) => {
    let [xp, yp, zp, xv, yv, zv] = [...stone.matchAll(/-?\d+/g)].map(el => +el[0])
    let [x1, y1, x2, y2] = [xp, yp, xp + xv, yp + yv]
    return total
  }, 0
)
Enter fullscreen mode Exit fullscreen mode

I'm seeing the values I expect.

Now it's time to add the loop and do the same for each unique pair of hailstones.

Loop and repeat parsing for line #2

I have to compare each hailstone.

That will require a loop.

But one that only compares pairs once.

Like this:

for (let i = 0; i < input.length - 1; i++) {
  for (let j = i + 1; j < input.length; j++) {
    // Compare
  }
}
Enter fullscreen mode Exit fullscreen mode

Of course, doing this means I need to re-think my reduce.

That's ok. It's a simple change.

Now I've got tons of 2-letter variables being thrown around though.

Oh well.

Here's my latest algorithm:

const stones = input.split('\n')
let total = 0
for (let i = 0; i < stones.length - 1; i++) {
  let [xp, yp, zp, xv, yv, zv] = [...stones[i].matchAll(/-?\d+/g)].map(el => +el[0])
  let [x1, y1, x2, y2] = [xp, yp, xp + xv, yp + yv]
  for (let j = i + 1; j < stones.length; j++) {
    let [xp, yp, zp, xv, yv, zv] = [...stones[j].matchAll(/-?\d+/g)].map(el => +el[0])
    let [x3, y3, x4, y4] = [xp, yp, xp + xv, yp + yv]
    let target = intersect(
      x1, y1, x2, y2, x3, y3, x4, y4
    )
  }
}
Enter fullscreen mode Exit fullscreen mode

Thankfully, I'm seeing all the X,Y coordinates - or mentions of parallel lines - that I expected.

My algorithm and Leo's algorithm are behaving correctly!

Next, I need to check for whether the intersection is inside of the bounding box.

In or out of scope

Pick two numbers that are not equal:

let min = 7
let max = 27
Enter fullscreen mode Exit fullscreen mode

Each of my intersection's coordinates must at least be within that range, inclusive of both min and max.

let [x, y] = Object.values(target)
if (x >= min && x <= max && y >= min && y <= max) {
  // Proceed to next test
}
Enter fullscreen mode Exit fullscreen mode

Running my program shows the expected text for my if...else clauses.

Past or future

The last test is whether the intersection occurs on a spot on either line that trends along the velocity, or trends away from it.

I'm struggling to think of a way to do this without overly complicated conditions.

Here's my over-conditional approach:

Given:
xp = A // stone 1's X position
xv = B // stone 1's X velocity
tx = C // intersection's X

if (C < A && A + B < A)
  Stone 1 is headed toward the intersection point
if (C < A && A + B > A)
  Stone 1 is headed away from the intersection point

And the opposite comparisons
if (C > A && A + B > A)
  Stone 1 is headed toward the intersection point
if (C > A && A + B < A)
  Stone 1 is headed away from the intersection point

And all four checks for the other stone

I need the first case to be true for all of them
Enter fullscreen mode Exit fullscreen mode

That's eight conditions, each with two checks.

Yeesh!

What about sorting those three values and checking for whether A is the first or last item?

Because if the intersection point is in the future, then A + B and C should trend the same direction: away from A.

Thus, A should either be less than or greater than both A + B and C.

If this proves true, I can cut my conditions to just two...I think!

Time to try!

It seems to work!

Here's the complex-seeming - but much smarter - algorithm:

let [A1, B1, C1] = [x1p, x1p + x1v, x]
let s1x = [A1, B1, C1].sort((a,b) => a - b).indexOf(x1p) !== 1
let [D1, E1, F1] = [x2p, x2p + x2v, x]
let s2x = [D1, E1, F1].sort((a,b) => a - b).indexOf(x2p) !== 1
let [A2, B2, C2] = [y1p, y1p + y1v, y]
let s1y = [A2, B2, C2].sort((a,b) => a - b).indexOf(y1p) !== 1
let [D2, E2, F2] = [y2p, y2p + y2v, y]
let s2y = [D2, E2, F2].sort((a,b) => a - b).indexOf(y2p) !== 1
if ([s1x, s2x, s1y, s2y].every(el => el == true)) {
  total += 1
}
Enter fullscreen mode Exit fullscreen mode

I compare to 1 because if the X or Y position is in the middle of the sorted array, then the intersection has occurred in the past (trending away from the velocity of the stone).

After running my algorithm, I see the expected answer of 2.

Am I ready to swap inputs, update bounding box dimensions, and see if I even generate an answer using my puzzle input?

...

I ran it.

I got an answer close to 20k.

Is it the correct answer?

...

It is!!!

Woohoo!!!

Thank you Paul and Leo for making this gold star possible for me to obtain!

Let's see how much harder Part 2 is...and if I'll need to use math.js instead for finding intersections in 3D.

Part 2

Jaw drops. How in the what now?

Another challenge in the theme of 'dream up the perfect candidate'.

I need to identify a line that intersects with all of the lines in my input.

And then identify an ideal point on that line.

Maybe I can look for a formula or algorithm to solve for that first part.

...

Couldn't find anything immediately.

I wish I could draw these lines no a 3D graph.

Sadly, I couldn't find an existing online tool to that easily.

My input has 300 points.

And now that I'm using the Z coordinate, those points - and the lines they create when moving - likely go in all sorts of directions.

So, even if I could see it, I doubt I would come up with the single point and line that crosses them all at the right time.

All that to say...it's time to move on to Day 25.

I'm happy I earned one gold star on Day 24.

I wasn't expecting Part 2 of Day 24 to be something I could solve, either.

As the cover image reiterates, special thanks to Paul and Leo for providing the necessary formulas and algorithms to make it possible for me to solve Part 1.

Onward to the final puzzle of the year!

Top comments (0)