These changes to instantiate new vectors don't materially impact the runtime of either of these days, so it seems best to make the immutability explicit.
I _can_, however, make day 24 twice as fast by manipulating vectors in-place, but that's an ugly optimization that isn't actually necessary given its already-decent runtime (100ms -> 50ms) at the expense of making the code uglier and less DRY, so for now I'm not going to do that.
This takes the runtime from 5-6s on part 2 to ~100ms by using a better data structure for lookups than trying to check if the Queue contains an existing state or not.
Now the slowest thing here is the GetNeighbors loop. I feel like that can be improved somehow, but I'm not sure if I can improve it and still retain its ergonomics or not. We shall see!
This caches all possible board states at parse time so that they can just be indexed into as needed. Part 2 still takes 5-6 seconds to run in the debugger (1-2 seconds outside the debugger, though), so there's obviously something I'm still missing here, but this makes a substantial dent in the runtime. Next step is actually running the profiler to get more info.
This is super slow, but it works. Talked through some speedup ideas with @tocchan that I'm going to try next.
Gonna be a BFS pro by the end of this advent...
This horrible monstrosity solves my input and probably only my input. I originally solved the sample, but didn't realize that my actual input had the cube in a different orientation. So that was about 45 minutes well spent trying to figure out the mappings of cube edges by hand. Then got stuck trying to debug the exact tile transitions from spot to spot...but I'd rather do all this than try to figure out 3d cube mapping...
I don't know why, but in a Profiling run, File.Exists() returns true for a file that very much does not exist. Adding the directory check enables it to get the actually-correct answer. Another option is handling an exception in File.ReadLines, but I'm ok with this for now.
This doesn't fix Rider's profiler on macos, though...on mac, it hangs forever thinking that input is redirected but not actually having anything in the buffer. I haven't found a fix for that yet.
Yes, I skipped day 19. For now. It sucks.
Day 20 is another "oh right, % is remainder in C#, not modulo" which means it does dumb things with negative numbers. I originally implemented this by hand, looping through the addition/subtraction and wrapping as necessary, but found "euclidean division" after a few Google searches for, essentially, "surely C# can do modulo somehow, right?" and finding that the answer is "no, not really" (though there is an open proposal to make %% a true modulo operator, but that doesn't help me now). So this math utility was the best thing I could find on Wikipedia that did what I wanted (I don't need both the division and modulo response, though, so I left the division part commented out for now). The other trick here is using the shortened length of the array when performing the modulo due to the rules around how wrapping works in this problem (which I think is terribly explained and confusing, but it's also late and I could just be dumb).
I think a faster solution is to just change indices instead of actually moving numbers around in the list, but I'm happy with this for now.
I spent an inordinately long amount of time on part 2, only to discover a fundamental flaw in my logic. Apparently for my input set, switching to the other person every time the first person had less time remaining meant that some combinations never got tested. The solution in this commit exhausts every option for one person, then every option for the second person (or animal...) which works for my test input. I have a feeling it won't work for all, though.
Some easy tweaks from the profiler's data that move part 1 from 200ms to 10ms (both by applying some previous part 2 optimizations and also from the same changes that improved part 2 further) and part 2 from 90ms to 60ms.
I always knew LINQ was trading speed for convenience, but I'm not sure I realized just how much the trade-off costs under some circumstances. It definitely still has its value and can be "fast" or at least "fast enough" if used properly.
This takes the time from 20s+ to 90ms. There were 2 slow places, both LINQ shortcuts I was taking to simplify a few problems. Keeping track of the current tower height can be done without finding the highest Y out of the entire tower repeatedly per rock, which was a big chunk of slowness, and finding cycles can be done without chunking things up and comparing every single chunk, which was the other part of the slowness (I can be reasonably sure that if 3 chunks in a row match, we probably found the cycle, at least with the sample input and my input).
Then a tiny amount of tuning the data to my input set (skipping 300 since it finds the loop starting at offset 309) and changing an unrolled multiplication loop into a plain multiply cleans up the last few bits.
Mostly from not testing and adding each individual vector, just the x component we care about. Some of the improvement came from setting capacity on the hash set up front. I could get even better if I was smarter about computing ranges of numbers rather than this dumb approach of putting each number into a hashset to function as a union/intersection surrogate.
This is mostly from reusing the same object instead of creating a new one each time we test a new point, but there's also a minor amount of speedup from using HashSet instead of Dictionary. I originally thought I would need to care about why each spot was blocked (wall vs sand) but it turns out that's not important for this problem.
I am not completely happy with this data structure, but it's functional enough. I wanted to implement comparers and equality overrides for the classes to make part 2 shorter, but it would have come at the cost of a bunch of boilerplate code, so whatever.
This enables a flood-fill of distances from the goal location to every other location on the grid. With that, we only have to locate the node with the lowest distance value.
Technically this also solves part 1, but I wanted to leave the forward search in place as a test of sorts that the algorithm works both ways.
Now if I can just get the parse sped up...
This is quite slow, at least for part 2 (and part 1 is slower than I want it to be, event parse takes longer than I thought it should). I need a better approach to the second part. Probably need to mark all nodes' distances from the goal point instead of one-by-one plotting a path from each possible start to the end, but that requires an adjustment to how I'm building the graph in the first place.
This is ugly. I probably should've vec2'd this, made smarter move and follow logic that isn't a series of 'if's, and found a smarter way to calculate distance than my hackneyed vector distance, but it totally works so I'm rolling with it.
I'm sure there's a cleaner way to search in four directions rather than manually running each loop like this, but I didn't want to mess with genericizing it just yet.
A Deep Copy and a double-ended queue would have made this day much easier. I should add those. (Having trouble finding a generic deep copy that works in .net 6, though...I opted to rewrite my original List<List<char>> solution into List<string> in lieu of a deep copy so I wouldn't have to parse the input all over again.)
This is an ugly, kind-of-brute-force solution. I'm not proud of it, but it works and got me top 1000 on both parts.
I also changed the "which day to run if no day is specified" to just always choose the highest numbered day.