Forklifts, paper rolls, and lots of adjacency checks: Day 4 turned into a neat graph-like puzzle. Here’s how I solved both parts and what their complexity looks like.
Part 1 – Finding accessible rolls
Each roll of paper is represented by @ on a grid. A roll is accessible if fewer than four of the eight neighboring cells also contain rolls. The solver:
- Parses the grid from
input.txt. - Walks every cell; when it finds an
@, it counts the occupied neighbors using the eight-direction offsets. - Increments the answer whenever the neighbor count is
< 4.
Complexity: Let the grid be H x W. We inspect each cell once and examine up to eight neighbors, so runtime is O(H * W) with O(1) auxiliary memory.
1const fs = require('fs');
2const path = require('path');
3
4const inputPath = process.argv[2] ?? path.join(__dirname, 'input.txt');
5const DIRECTIONS = [
6 [-1, -1], [-1, 0], [-1, 1],
7 [0, -1], /*self*/ [0, 1],
8 [1, -1], [1, 0], [1, 1],
9];
10
11function readGrid(filePath) {
12 try {
13 return fs
14 .readFileSync(filePath, 'utf8')
15 .trim()
16 .split(/\r?\n/)
17 .map((line) => line.trim())
18 .filter((line) => line.length > 0);
19 } catch (error) {
20 throw new Error(`Unable to read input file: ${filePath}`);
21 }
22}
23
24function countAccessibleRolls(grid) {
25 const height = grid.length;
26 const width = grid[0]?.length ?? 0;
27
28 let count = 0;
29 for (let y = 0; y < height; y += 1) {
30 for (let x = 0; x < width; x += 1) {
31 if (grid[y][x] !== '@') {
32 continue;
33 }
34
35 let adjacent = 0;
36 for (const [dy, dx] of DIRECTIONS) {
37 const ny = y + dy;
38 const nx = x + dx;
39 if (ny < 0 || ny >= height || nx < 0 || nx >= width) {
40 continue;
41 }
42 if (grid[ny][nx] === '@') {
43 adjacent += 1;
44 }
45 }
46
47 if (adjacent < 4) {
48 count += 1;
49 }
50 }
51 }
52
53 return count;
54}
55
56function main() {
57 if (!fs.existsSync(inputPath)) {
58 console.error(`Input file not found: ${inputPath}`);
59 process.exitCode = 1;
60 return;
61 }
62
63 const grid = readGrid(inputPath);
64 if (grid.length === 0) {
65 console.log('0');
66 return;
67 }
68
69 const result = countAccessibleRolls(grid);
70 console.log(result.toString());
71}
72
73main();
Part 2 – Removing rolls in waves
Part 2 asks for the total number of rolls that can be removed if we repeatedly remove any roll that’s currently accessible (i.e., has fewer than four neighbors). This is essentially a flood of “peeling” operations.
Steps:
- Precompute, for every roll cell, how many neighboring rolls it has and store a boolean
activeflag. - Push every roll with
< 4neighbors into a queue. - Pop from the queue; if the roll is still active and below the neighbor threshold, remove it. After removal, decrement the neighbor counts of its live neighbors and enqueue any that just fell below the threshold.
- Continue until the queue empties; the removal counter is the answer.
This is equivalent to a BFS/queue-based propagation. Every cell enters the queue a bounded number of times and every edge (adjacent pair) is visited when counts change.
Complexity: Each roll is marked and potentially enqueued a few times, but every adjacency update happens at most once per removal. This keeps the runtime at O(H * W) with O(H * W) storage for the neighbor counts, active flags, and queue bookkeeping.
1const fs = require('fs');
2const path = require('path');
3
4const DIGITS_TO_SELECT = 12;
5const inputPath = process.argv[2] ?? path.join(__dirname, 'input.txt');
6
7function readBanks(filePath) {
8 try {
9 return fs
10 .readFileSync(filePath, 'utf8')
11 .trim()
12 .split(/\r?\n/)
13 .map((line) => line.trim())
14 .filter((line) => line.length > 0);
15 } catch (error) {
16 throw new Error(`Unable to read input file: ${filePath}`);
17 }
18}
19
20function bestTwelveDigits(bank) {
21 if (bank.length <= DIGITS_TO_SELECT) {
22 return BigInt(bank);
23 }
24
25 const stack = [];
26 let remaining = bank.length - DIGITS_TO_SELECT;
27
28 for (const char of bank) {
29 const digit = char;
30 while (stack.length && remaining > 0 && stack[stack.length - 1] < digit) {
31 stack.pop();
32 remaining -= 1;
33 }
34 stack.push(digit);
35 }
36
37 while (stack.length > DIGITS_TO_SELECT) {
38 stack.pop();
39 }
40
41 return BigInt(stack.join(''));
42}
43
44function solve(banks) {
45 return banks.reduce((sum, bank) => sum + bestTwelveDigits(bank), 0n);
46}
47
48function main() {
49 if (!fs.existsSync(inputPath)) {
50 console.error(`Input file not found: ${inputPath}`);
51 process.exitCode = 1;
52 return;
53 }
54
55 const banks = readBanks(inputPath);
56 if (banks.length === 0) {
57 console.log('0');
58 return;
59 }
60
61 const result = solve(banks);
62 console.log(result.toString());
63}
64
65main();
Takeaways
- Both parts rely on the same grid parsing and eight-direction neighbor logic, so
part2.jslargely reuses the constants and helpers frompart1.js. - Treating the removal process like a queue-driven cascade avoids repeated rescans of the grid.
- The constraints are friendly enough that a straightforward implementation in Node stays fast, even for large inputs.
With the forklifts now able to reach (and remove) everything they need, the path to the cafeteria is clear.