-
Notifications
You must be signed in to change notification settings - Fork 0
/
21-1.ts
277 lines (243 loc) Β· 9.79 KB
/
21-1.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
/* --- Day 21: Step Counter ---
You manage to catch the airship right as it's dropping someone else off on their
all-expenses-paid trip to Desert Island! It even helpfully drops you off near the gardener
and his massive farm.
"You got the sand flowing again! Great work! Now we just need to wait until we have enough
sand to filter the water for Snow Island and we'll have snow again in no time."
While you wait, one of the Elves that works with the gardener heard how good you are at
solving problems and would like your help. He needs to get his steps in for the day, and
so he'd like to know which garden plots he can reach with exactly his remaining 64 steps.
He gives you an up-to-date map (your puzzle input) of his starting position (S), garden
plots (.), and rocks (#). For example:
...........
.....###.#.
.###.##..#.
..#.#...#..
....#.#....
.##..S####.
.##..#...#.
.......##..
.##.#.####.
.##..##.##.
...........
The Elf starts at the starting position (S) which also counts as a garden plot. Then, he
can take one step north, south, east, or west, but only onto tiles that are garden plots.
This would allow him to reach any of the tiles marked O:
...........
.....###.#.
.###.##..#.
..#.#...#..
....#O#....
.##.OS####.
.##..#...#.
.......##..
.##.#.####.
.##..##.##.
...........
Then, he takes a second step. Since at this point he could be at either tile marked O, his
second step would allow him to reach any garden plot that is one step north, south, east,
or west of any tile that he could have reached after the first step:
...........
.....###.#.
.###.##..#.
..#.#O..#..
....#.#....
.##O.O####.
.##.O#...#.
.......##..
.##.#.####.
.##..##.##.
...........
After two steps, he could be at any of the tiles marked O above, including the starting
position (either by going north-then-south or by going west-then-east).
A single third step leads to even more possibilities:
...........
.....###.#.
.###.##..#.
..#.#.O.#..
...O#O#....
.##.OS####.
.##O.#...#.
....O..##..
.##.#.####.
.##..##.##.
...........
He will continue like this until his steps for the day have been exhausted. After a total
of 6 steps, he could reach any of the garden plots marked O:
...........
.....###.#.
.###.##.O#.
.O#O#O.O#..
O.O.#.#.O..
.##O.O####.
.##.O#O..#.
.O.O.O.##..
.##.#.####.
.##O.##.##.
...........
In this example, if the Elf's goal was to get exactly 6 more steps today, he could use
them to reach any of 16 garden plots.
However, the Elf actually needs to get 64 steps today, and the map he's handed you is much
larger than the example map.
Starting from the garden plot marked S on your map, how many garden plots could the Elf
reach in exactly 64 steps? */
// What we immediately notice with the example illustrations above is that the places we
// can reach in 2 steps is the superset of the places we can reach in 0 steps and the
// places we can reach in 2 steps _without backtracking_. Likewise, the number we could
// reach in 3 is the superset of the number reachable in 1 step and the number reachable
// in 3 steps without backtracking. Thus, to find the places we could reach in 64 steps,
// we could build up the set of places we can reach in 2 steps, then 2 steps from that
// without backtracking, then 2 steps from that without backtracking, etc. until we reach
// 64 steps total!
export type Position = { row: number; column: number };
type Direction = "N" | "S" | "E" | "W";
// This function does the main work; it takes as inputs a garden map, starting position,
// and number of steps to follow per path. It then traces all the possible paths through
// from the starting position up to `numberOfSteps` away *without backtracking*, then
// returns the list of positions it reached as well as the updated map with plots that
// were visited marked as "V".
function spotsReachableNoBackTrack(
gardenMap: string[],
startingPos: Position,
numberOfSteps = 2
): [updatedMap: string[], reachablePlots: Position[]] {
const reachablePlots: Position[] = [];
// Make a copy of the map so we can update it to mark garden plots as visited.
let updatedMap = [...gardenMap];
// We'll only move one step at a time, so we'll keep a queue of partially-followed paths
// we still have to finish exploring, starting with our inputs.
const incompletePaths: [currPos: Position, stepsLeft: number][] = [
[startingPos, numberOfSteps],
];
// While we still have paths left to finish following...
while (incompletePaths.length) {
// Take the next incomplete path.
const [currPos, stepsLeft] = incompletePaths.shift()!;
// Check what occupies the plot in each cardinal direction.
for (const direction of ["N", "S", "E", "W"] as const) {
const { row, column } = move(currPos, direction);
const plot = updatedMap[row]?.[column];
// We can only move here if the plot is unvisited garden.
if (plot === ".") {
// Update the row on the map to mark that this plot has now been visited.
updatedMap[row] =
updatedMap[row]!.slice(0, column) +
"V" +
updatedMap[row]!.slice(column + 1);
// If this is the end of the path (there would be no more steps left), record this
// as a new reachable position.
if (stepsLeft - 1 === 0) {
reachablePlots.push({ row, column });
} else {
// Otherwise, add this new position and the remaining steps to the queue to keep following.
incompletePaths.push([{ row, column }, stepsLeft - 1]);
}
}
}
}
return [updatedMap, reachablePlots];
}
// Helper function which takes a starting `Position` and a `Direction` and returns the new
// `Position` that moving one step in that direction would lead to.
function move({ row, column }: Position, direction: Direction): Position {
switch (direction) {
case "N":
return { row: row - 1, column };
case "S":
return { row: row + 1, column };
case "E":
return { row, column: column + 1 };
case "W":
return { row, column: column - 1 };
}
}
// This function will perform the "outer loop", finding the total set of plots
// reachable in `numberOfSteps` on a garden map with backtracking allowed by combining
// together the plots reachable by non-backtracking paths of every increment of 2 up
// to `numberOfSteps`. It returns the resultant list of all plots reachable.
export function spotsReachable(
gardenMap: string[],
startingPos: Position,
numberOfSteps = 64
): number {
// If `numberOfSteps` is even, we want to look for plots reachable in even numbers
// of steps (0, 2, 4...`numberOfSteps`). If `numberOfSteps` is odd, we want to look for
// plots reachable in odd numbers of steps (1, 3, 5...`numberOfSteps`). Thus, if
// `numberOfSteps` is even, initially our only reachable plot is the starting
// position, which we can reach in 0 steps. However, if `numberOfSteps` is odd, we
// initially look for plots reachable in 1 step.
let [updatedMap, initialPlotsReachable] =
numberOfSteps % 2 === 0
? [[...gardenMap], [startingPos]]
: spotsReachableNoBackTrack(gardenMap, startingPos, 1);
// For debugging, we'll keep all reachable plots in this array. It will start with the
// initial set we could reach in 0 or 1 steps.
// const allPlotsReachable: Position[] = [...initialPlotsReachable];
// For performance reasons, if `returnArray` is false, we'll actually just return the
// count of plots instead.
let allPlotsReachableCount: number = initialPlotsReachable.length;
// We need to track how many steps away we've looked in total, since we'll only follow
// paths up to 2 away at a time. We start with the appropriate number of steps away for
// if our `numberOfSteps` was even or odd.
let totalStepsTraveled = numberOfSteps % 2 === 0 ? 0 : 1;
// We also need to track the last set of reachable plots, so that we know where to
// start from when we move 2 more steps forward.
let lastPlotsReached: Position[] = [...initialPlotsReachable];
while (totalStepsTraveled < numberOfSteps) {
// For each plot we were last able to reach, use it as a new starting position and
// look for the plots reachable 2 more steps away.
const nextPlotsReachable: Position[] = [];
for (const position of lastPlotsReached) {
let plots: Position[];
[updatedMap, plots] = spotsReachableNoBackTrack(updatedMap, position);
// Record these plots as reachable.
// allPlotsReachable.push(...plots);
allPlotsReachableCount = allPlotsReachableCount + plots.length;
nextPlotsReachable.push(...plots);
}
// Once we've explored 2 steps away for all of `lastPlotsReached`, update our total
// steps traveled and repeat the process for `nextPlotsReachable`.
totalStepsTraveled = totalStepsTraveled + 2;
lastPlotsReached = nextPlotsReachable;
}
return allPlotsReachableCount;
}
const TEST_MAP = [
"...........",
".....###.#.",
".###.##..#.",
"..#.#...#..",
"....#.#....",
".##..S####.",
".##..#...#.",
".......##..",
".##.#.####.",
".##..##.##.",
"...........",
];
const testSpotsReachable = spotsReachable(TEST_MAP, { row: 5, column: 5 }, 6);
console.log(
testSpotsReachable === 16 ? "β
" : "β, expected 16 but got",
testSpotsReachable
);
// Now try for our real garden map input.
import * as fs from "fs";
fs.readFile("./2023/21.txt", (err, rawFile) => {
if (err) throw err;
const map = rawFile.toString().split("\n");
// Locate our starting position.
let startingPos: Position = { row: -1, column: -1 };
let rowIndex = 0;
while (rowIndex < map.length) {
const row = map[rowIndex]!;
if (row.indexOf("S") !== -1) {
startingPos = { row: rowIndex, column: row.indexOf("S") };
break;
}
rowIndex = rowIndex + 1;
}
if (startingPos.row === -1 || startingPos.column === -1) {
throw new Error("could not locate starting position in input");
}
console.log("part 1:", spotsReachable(map, startingPos));
});