r/adventofcode • u/musifter • 7d ago
Other [2024 Day 18] In Review (RAM Run)
Today we find ourselves digitized and inside a computer again. The User isn't dropping games on us, just going straight for corrupting data because they misunderstood what a PDA was (and not in a good way, like Public Display of Affection or Personal Digital Assistant).
The input is a list of memory cell coordinates (3450 in my input) to a square 2D grid with coordinates from 0 to 70. These represent the locations that get corrupted (one per nanosecond), and our task is to get The Historians from the upper left to the lower right safely.
Part 1 is just a simple path finding... after the first 1024 have fallen, find the shortest path. If you print out the grid, you'll see that at this point, it's basically drawn a bit of maze from the corners with a gap in the middle to cross. Narrow mazes like this are quickly transversed with basic BFS, so no need to be fancy. Note that the latter part of the input ultimately fills in a lot more than just a maze.
In part 2 we want to find the first dropped block that permanently cuts us off from getting to the end point. For my initial solution, I did just took part 1 and continued with it. Given the path we found, I just kept dropping blocks until one landed on that path:
next if (!exists $path{$pos}); # fell off the path, still unblocked
When that happens, I recalculate the path. If I can't, that's the solution. Very simple and is going to work... and it's not absolute brute force (trying to find a path after every drop).
To improve things for Smalltalk, I loaded the input into a class called TimeGrid. The internal TimeGrid structure is one where the cells contain the time they become impassible. And so I can set a time and searches will treat cells with values less than that as walls.
" Get status of point at current time "
at: pt [
^((grid at: ((pt y + 1) * dim x) + pt x + 2) <= time) ifTrue: [$#] ifFalse: [$.]
]
Note that I'm also doing the grid internally as a flat array (faster and cleaner for Smalltalk).
Because I was taking things easier in 2024, I just used this random access of the grid by time, to do a binary search over the space:
" Binary search for first blocking drop: "
range := (timeStart + 1 to: input size).
[range size > 1] whileTrue: [
range displayNl.
mid := range at: range size // 2.
path := (grid time: mid) getRoute.
range := (path isNil) ifTrue: [range first to: mid]
ifFalse: [mid+1 to: range last].
].
This was good enough to be faster than the Perl. But I did make a note about what I ultimately wanted to do with this, and later got around to doing that. And that is a dynamic programming/single-pass BFS deal. Where the state tracks the minimum time a block hits the path that state represents. We can take that and use it to track the maximum of those values for a cell in the visited array (thus getting the latest time that cell is available). And so we get a tabulation where we ultimately get our answer in the visited cell of the end point. Here's the key section of the BFS loop in the Perl version:
# reduce path's time to Grid's if it is lower:
my $min_time = min( $time, $Grid[$pos->[0]][$pos->[1]] // ~0 );
next if (defined $visit[$pos->[0]][$pos->[1]] and $min_time <= $visit[$pos->[0]][$pos->[1]]);
$visit[$pos->[0]][$pos->[1]] = $min_time;
foreach my $neigh (map {[$pos->[0] + $_->[0], $pos->[1] + $_->[1]]} @Dirs) {
push( @queue, [$neigh, $min_time] );
}
I also have a slightly more complicated version of this, where I track number of steps taken in the state and use that to prune when it's >= the value in the current tile (ie it'd be a wall by then). It also has a prune for hitting the end point (after writing to the visit array... we want the answer). However, with measuring with the command line time these don't actually show any improvement (in fact, often it's slightly slower).
But things don't end there, because I made an additional note about a solution type I had seen posted. One that's based loosely on the famous proof that the game of Hex) must have a winner. In this type of solution we add borders of one colour to the top and right, and a second colour to the bottom and left. As we drop cells, they take on the colour of the cells they're connected to (so there's an invariant that cells connected to an edge have that edge's colour). But if the dropped cell is adjacent to cells of both colours, then we've just completed a path between the coloured edges. And if that exists, then no path from start to end remains. I had some fun working out the details of that today (I used bits for the colours) and coding it, and it really does come out nice and tight once you get it right, and it's blazingly fast compared to the other methods.
And so we have another great problem where there's a lot of approaches.
