This day's problem seems fairly simple, at least for part 1. It's simply a line-line intersection. Because time is ignored, each "hailstone" is just a line, and I'm only testing for intersections within the X and Y axis. I'm sure part 2 will include the whole of the data, but I don't mind getting Part 1 out of the way quickly.
For the most part, I am going to be writing my intersection test function based on this Wikipedia article. I know that this would only really work in 2D; for part 2 I have a different solution in mind.
struct Hailstone {
double x, y, dx, dy;
const double xySlope() const {
if (dx == 0) return DBL_MAX;
return (double)dy / (double)dx;
}
};
bool testIntersection(Hailstone l, Hailstone r, pair<double, double>& intersection)
{
auto m1 = l.xySlope();
auto c1 = l.y - m1 * l.x;
auto m2 = r.xySlope();
auto c2 = r.y - m2 * r.x;
if (m1 == m2) return false;
intersection.first = (double)(c2 - c1) / (m1 - m2);
intersection.second = m1 * intersection.first + c1;
return true;
}
That returns the intersection point (if any) of the lines. From there, just reject any collisions that happen outside of the test area or in the past for either hailstone. Combined with the main loop testing for the area and temporal components:
uint64_t totalIntersections = 0;
for (int i = 0; i < hailstones.size(); i++)
{
for (int j = i + 1; j < hailstones.size(); j++)
{
Hailstone l = hailstones[i], r = hailstones[j];
pair<double, double> intersection;
if (testIntersection(l, r, intersection))
{
// Test outside of test area
if (intersection.first < AREA_MIN || intersection.first > AREA_MAX ||
intersection.second < AREA_MIN || intersection.second > AREA_MAX)
continue;
// Test if intersection is in past
if (l.dx > 0 && intersection.first < l.x) continue;
if (r.dx > 0 && intersection.first < r.x) continue;
if (l.dx < 0 && intersection.first > l.x) continue;
if (r.dx < 0 && intersection.first > r.x) continue;
totalIntersections++;
}
}
}
That does the trick nicely
... Ugh. I wasn't expecting this. For part 2, first, all 3 dimensions need to be taken into account, which I epxected. But the actual solution requires adding one more to the list, to make it intersect with every hailstone. This sounds like it will be a computational pain.
Hmm. Thinking about this a bit more... the time component is nice, but considering how scattered the lines are (zero are intersecting currently), I just need to fine one line that intersects all of the hailstones first. The time component can come later to figure out the exact part of that line to start.
I will admit, the math mostly eludes me for this. Linear Algebra was never something I really studied seriously. Conceptually I undertand that I need just three lines to find the intersection - one would allow functionally any other line, two would have a curved plane between them - but I don't know the math to describe what I can see in my head.
What if I just look for similarities? If there's a pair of hailstones with a matching velocity in the same axis, then they'll be in lockstep for that direction, with their distance the same. There should only be a few valid velocities that would hit them both; basically some valid divisor of their distance.
Pairs: [x: 121, y:65, z:89]
That seems like a validly small number of pairs to check.
Distance % (X - V) = 0
. There will be a lot of values for X that would satisfy, but I doubt the velocities would get much greater than what's in the input file. If I search from -500 to 500 that likely would cover it. For each pair in each axis, I'll find the set of valid values of X, and intersect that with all previously found pairs; repeat until the set is of size 1. That should be the velocity!
set<int64_t> getVelocities(int64_t distance, int64_t hailVelocity)
{
set<int64_t> valid;
if (distance < 0) distance *= -1;
for (int i = -500; i <= 500; i++)
{
if (i == hailVelocity) continue;
if (distance % (i - hailVelocity) == 0)
{
valid.insert(i);
}
}
return valid;
}
// ---
set<int64_t> xV, yV, zV;
for (int i = 0; i < hailstones.size(); i++)
{
for (int j = i + 1; j < hailstones.size(); j++)
{
Hailstone l = hailstones[i], r = hailstones[j];
int matchCount = 0;
if (l.dx == r.dx)
{
auto pairX = getVelocities(r.x - l.x, r.dx);
if (xV.size() == 0) xV = pairX;
set<int64_t> xIntersect;
set_intersection(xV.begin(), xV.end(), pairX.begin(), pairX.end(), inserter(xIntersect, xIntersect.begin()));
xV = xIntersect;
}
if (l.dy == r.dy) { } // Repeat...
if (l.dz == r.dz) { } // Repeat...
}
}
Continuously intersecting them... I get exactly one velcity that works for all pairs! Therefore that must be the single velocity!
Now to find the starting position for the rock, it's just a modified form of the slope equality equations I used for part 1; basically finding the relative slopes for the first two hailstones.
// Assumption - Exactly one valid X, Y, and Z velocity was found
Hailstone a = hailstones[0], b = hailstones[1];
double dx = *xV.begin(), dy = *yV.begin(), dz = *zV.begin();
double ma = (a.dy-dy)/(a.dx-dx);
double mb = (b.dy-dy)/(b.dx-dx);
double ca = a.y - (ma*a.x);
double cb = b.y - (mb*b.x);
double rx = (cb-ca)/(ma-mb);
double ry = ma*rx + ca;
double rt = (rx - a.x)/(a.dx-dx);
double rz = a.z + (a.dz-dz) * rt;
rx
, ry
, and rz
end up being the final rock position coordinates.
I will admit, I had to look on Reddit for a bit for this one, purely for the math. I was able to intuit some stuff myself, but not everything. Plus, having a reference helped me discover a bug in the mathy code at the end; the math was correct, but I transposed a term that ended up giving close, but not perfect results.