You try contacting the Elves using your handheld device, but the river you're following must be too low to get a decent signal.
You ask the device for a heightmap of the surrounding area (your puzzle input). The heightmap shows the local area from above broken into a grid; the elevation of each square of the grid is given by a single lowercase letter, where a
is the lowest elevation, b
is the next-lowest, and so on up to the highest elevation, z
.
Also included on the heightmap are marks for your current position (S
) and the location that should get the best signal (E
). Your current position (S
) has elevation a
, and the location that should get the best signal (E
) has elevation z
.
You'd like to reach E
, but to save energy, you should do it in as few steps as possible. During each step, you can move exactly one square up, down, left, or right. To avoid needing to get out your climbing gear, the elevation of the destination square can be at most one higher than the elevation of your current square; that is, if your current elevation is m
, you could step to elevation n
, but not to elevation o
. (This also means that the elevation of the destination square can be much lower than the elevation of your current square.)
For example:
Sabqponm
abcryxxl
accszExk
acctuvwj
abdefghi
Here, you start in the top-left corner; your goal is near the middle. You could start by moving down or right, but eventually you'll need to head toward the e
at the bottom. From there, you can spiral around to the goal:
v..v<<<<
>v.vv<<^
.>vv>E^^
..v>>>^^
..>>>>>^
In the above diagram, the symbols indicate whether the path exits each square moving up (^), down (v), left (<), or right (>). The location that should get the best signal is still E
, and .
marks unvisited squares.
This path reaches the goal in 31 steps, the fewest possible.
What is the fewest steps required to move from your current position to the location that should get the best signal?
Your puzzle answer was 528.
As you walk up the hill, you suspect that the Elves will want to turn this into a hiking trail. The beginning isn't very scenic, though; perhaps you can find a better starting point.
To maximize exercise while hiking, the trail should start as low as possible: elevation a
. The goal is still the square marked E
. However, the trail should still be direct, taking the fewest steps to reach its goal. So, you'll need to find the shortest path from any square at elevation a
to the square marked E
.
Again consider the example from above:
Sabqponm
abcryxxl
accszExk
acctuvwj
abdefghi
Now, there are six choices for starting position (five marked a
, plus the square marked S that counts as being at elevation a
). If you start at the bottom-left square, you can reach the goal most quickly:
...v<<<<
...vv<<^
...v>E^^
.>v>>>^^
>^>>>>>^
This path reaches the goal in only 29 steps, the fewest possible.
What is the fewest steps required to move starting from any square with elevation a to the location that should get the best signal?
Your puzzle answer was 522.
This problem can be solved with just a Queue and breadth-first-search. No Dijkstra needed. But for brevity I'll show the Dijkstra as well.
The tips are highlighted in the code below, but a summary of it is:
- If the PriorityQueue is minimizing steps to a node, or something where every node has the same weight on its edges, use a regular Queue. This will make the solution BFS, not Dijkstra.
- Mark the starting node as visited.
- Check for your exit node before and outside of the for-loop.
- Your visited set is really "visitedOrWillVisit", because you should be adding to this set inside the for-loop, so you technically haven't visited it yet.
- Re-add to the visited collection if this new way to get the visited has a smaller weight.
function getMinStepsToEnd({ grid }: Simulation) {
type QueueItem = { point: Coordinate; steps: number; elevation: number };
const queue = new PriorityQueue<QueueItem>((item) => -item.steps);
const visited = new GenericSet<Coordinate>((c) => c.toString());
const ending = grid.getEnding();
queue.enqueue({ ...grid.getStarting(), steps: 0 });
// Make sure to add the starting node in visited before the while-loop.
visited.add(grid.getStarting().point);
while (!queue.isEmpty()) {
const { point, steps, elevation } = queue.dequeue()!;
// For any BFS or Dijkstra, make sure your return statement is here, and NOT
// in the for-loop below. Your queue will dequeue the best solution. In your
// for-loop, it might meet your exit condition, but it might be suboptimal.
if (elevation === ending.elevation) {
return steps;
}
const squares = grid.getAccessibleSquares(point);
const nextSteps = steps + 1;
for (const { elevation, point } of squares) {
// If the priority queue heap was min/max on something other than steps
// (which BFS already does, and why this could just be a BFS solution),
// your visited collection should be a MAP, not a Set! E.g.,
// if (!visited.has(point) || visited.get(point) > measurement)
// In other words, even if it's visited, if this visitation of the node
// has a better solution, then add it to your queue.
if (!visited.has(point)) {
queue.enqueue({ elevation, point, steps: nextSteps });
// visited should be added inside the for-loop, not outside of the
// for-loop. Otherwise your Set/Map will explode and youll get a out of
// memory exception.
visited.add(point);
}
}
}
return -Infinity;
}
The code below is the BFS solution. The tips from Dijkstra all apply here. The only additional tip is the return statement, which is highlighted in the comments below.
export function getMinStepsToEnd2({ grid }: Simulation) {
type QueueItem = { point: Coordinate; steps: number; elevation: number };
const queue = new Queue<QueueItem>();
const visited = new GenericSet<Coordinate>((c) => c.toString());
const ending = grid.getEnding();
queue.enqueue({ ...grid.getStarting(), steps: 0 });
visited.add(grid.getStarting().point);
while (!queue.isEmpty()) {
const { point, steps, elevation } = queue.dequeue()!;
// Returning at the first exit node usually isn't correct for BFS, but
// because we're trying to optimize steps from starting node, BFS will
// naturally optimize that so we can exit as soon as we get there.
if (elevation === ending.elevation) {
// Usually you should do
// min = Math.min(min, steps);
// but that isn't necessary because we're just minimizing steps, not some
// other parameter.
return steps;
}
const squares = grid.getAccessibleSquares(point);
const nextSteps = steps + 1;
for (const { elevation, point } of squares) {
if (!visited.has(point)) {
queue.enqueue({ elevation, point, steps: nextSteps });
visited.add(point);
}
}
}
return -Infinity;
}