8
votes
Day 10: Monitoring Station
Today's problem description: https://adventofcode.com/2019/day/10
Join the Tildes private leaderboard! You can do that on this page, by entering join code 730956-de85ce0c
.
Please post your solutions in your own top-level comment. Here's a template you can copy-paste into your comment to format it nicely, with the code collapsed by default inside an expandable section with syntax highlighting (you can replace python
with any of the "short names" listed in this page of supported languages):
<details>
<summary>Part 1</summary>
```python
Your code here.
```
</details>
This one's one of those problems that's easy to describe but very difficult to code.
This code is dirty, but I don't think I'll try to clean it up at all.
Part 1 Strategy
We're going to store the asteroids in a grid, where, luckily, array notation and the positions are (almost) identical.
Instead of doing anything smart, just try out every possible asteroid position and see how many asteroids one can scan from that particular asteroid.
This is achieved by doing subtraction of coordinates for relative position, then getting a direction vector by dividing out the GCD of the (absolute value of the) two coordinates. This guarantees that all lines that are going in the same direction have exactly the same direction vector, so we only need to then find the number of distinct elements.
We can do this by sticking all the direction vectors into a hash set (or tree set as I did).
Part 2 Strategy
This one really sucks, because sorting by angle is terrible if it's not specifically part of the language.
If you're clever or lucky, you probably have stuff like
sin
andcos
, which you can use to find slope and thus angle and sort that way.I'm not clever but I have
atan2
which, long story short, lets one get the counterclockwise angle of a vector relative to one fixed line.In our case, we take out all of our distinct direction vectors and then sort them using
atan2
,after which we must identify where "vertical up" is (it's <0,-1>) and then "cycle" the array to make sure it starts there (it turns out my array already started there) and possibly reverse it if you sorted the wrong way (I did).Finally, to get the 200th asteroid, we literally simulate the laser by cycling through this sorted list of direction vectors and stretching it out from our recorded origin point until it either hits an asteroid and goes out of bounds. With the right processing (setting the asteroid to false, using a counter, etc.) we will have a position coordinate at the end where at last we can process the coordinates of that asteroid and get the answer.
If you look at my code, there are some even crappier details (a custom implementation of atan, requiring that <0,-1> is even in the slopes, swapping x and y coords, etc.) but those are just me kludging an implementation of this strategy.
Solution Code (Parts 1/2)
Part 2 Spoilers
I'm not sure what your Part 1 answer was, but mine was high enough that I didn't actually have to worry about removing asteroids from the list. It would hit 200 well before making it all the way around, which saved me a fair number of headaches.
Part 2 Spoilers Reply
My Part 2 solution involved actually simulating the laser, so while there were indeed 200 asteroids there to destroy, I had to destroy them in the simulation, which meant doing a bit of extra processing with the asteroid grid.Edit: I just realized that you're talking about simplifying the code, and you're 100% right that the destruction code is superfluous (I had >200 unique asteroids visible). I guess it's just there to make the simulation accurate now.
Part 2 Spoilers Continued
Yeah, I was suggesting it might be easier that way. That being said, if we use this again on another day, actually destroying the asteroids may make give you a solid advantage depending on what 'Part 3' is.
This was definitely a tough one, it used some math that I haven't touched in a while
Part 1 - Python
For Part 1 for each asteroid I calculated the 'offset' for every other asteroid. Then I looked at the ratios of those offsets and eliminated any that are duplicates. For example if the asteroid I'm looking at sees an asteroid at (+1, +3) and another one at (+2, +6) those are at the same angle, so I eliminate the duplicate. The length of the remaining list is the number of other asteroids it can seePart 2 - Python
Part 2 was substantially more difficult. I improved the 'angle' method I developed for part 1 to tell me exactly which asteroid was closest along the path. Then I actually calculated the actual angle of every asteroid from the top position. Then I sorted the list based on the angle (and did some list manipulation). Once I had my ordered list, I was able to just find the 199th (0-indexed) element in it.Ooof, I lost a great deal of time to not correctly understanding the problem.
Spoilers
I've spent too much time playing (and some time writing) text-based roguelikes that use
#
as a wall character, and interpreted the problem as a typical roguelike-grid LOS problem where each#
blocks LOS entirely. I spent a chunk of time writing a line-walking algorithm and inspecting each tile on the line to see if it occluded or not.Setting up and testing this took a bit of time, and it was working "correctly" until I examined it against the test cases from AoC, which were completely different, since the "asteroids" are actually more-or-less sizeless points that only occlude something if they are exactly behind each other.
This means that the important piece of information about any given asteroid is not what grid tile it's on, but the angle to that asteroid from your base station (or candidate). If any two asteroids share the exact same angle from the base, then the nearer one is occluding the further one; in any other cases both are visible, even if the LOS passes through the same tile.
Once I understood how it worked, I switched to using a hashmap of
angle => [points]
and the problem opened up.Day 10 Crystal
This one was difficult in terms of visualizing the problem and making sure I understood quadrants correctly. Once I did that I was able to solve it, though it's still pretty ugly.
Part 1, Python
Part 2 Additional Code, Python
I don't think I solved this in a terribly good way, but I just took everything, divided it into quadrants to reduce ambiguity, and then sorted them in a way that would mean if I kept track of what had been destroyed overall and in that rotation I would be okay.
This one was challenging. I spent way too long trying to think of a solution better than O(n^2) for the first part, and then realized the naive solution of comparing every asteroid pair-wise worked well enough for the data size, and it looks like any better-than-naive algorithm for part 1 wouldn't help with part 2 anyway.
For part 2, I iterated over every asteroid, calculated the angle from the monitoring station to the asteroid, and then put it in a list of asteroids sharing that angle, and put that list in a Rust BTreeMap. (It's like a standard HashMap, except that it lets you iterate over the keys in order. If I didn't have a BTreeMap available, I could have just used a HashMap, and then made a list of its keys, sorted that, and iterated over that instead.) Then I repeatedly iterated over the BTreeMap in the order of the angles, and for each list, I removed one item (and then removed the list from the BTreeMap if it was empty) and then continued on until the BTreeMap was empty. I think this is an optimal algorithm for this kind of problem.
The amount of raw math involved in this one made it really difficult. I had an especially tough time getting the angle calculation right for the second part, due to a combination of factors:
-y
, not+y
.Atan2
returns values in the range[-π,π]
(in a disjoint way—if y ≥ 0,Atan2
returns a positive; ify
< 0 it returns a negative) but we need to have the range[0,2π]
in order for sorting the list of polar coordinates to work for this problem.This would have been a good candidate for test-driven development, but instead I was stubborn and went through a lot of trial and error, and graph paper.
I also spent some extra time making my GCD (greatest common denominator) function memoized, and it turned out to only save maybe 20 nanoseconds overall, if anything, since Go is just that fast.
Things I learned from this puzzle:
epsilon = math.Nextafter(n1, n2) - n1
.GitHub link with sane tab widths
Parts 1 and 2, Go