I spent Day 3 hanging out in the lobby with a pile of battery banks and a stubborn escalator. Here is how I cracked both puzzles using a pair of lean JavaScript scripts (part1.js and part2.js).
Part 1 – Two batteries, one number
Each bank is a string of digits. I needed the largest possible two-digit value that preserves order. Rather than evaluate every pair, I sweep the string once while tracking the best “first” digit seen so far. Every new digit forms a candidate pair with that best digit; if the candidate beats the running maximum, I store it, and if the new digit is even better as a first digit, I promote it. This greedy pass extracts the answer for a bank in linear time.
Complexity: For n banks whose lengths sum to L, the runtime is O(L) and the extra memory stays at O(1).
1const fs = require('fs');
2const path = require('path');
3
4const inputPath = process.argv[2] ?? path.join(__dirname, 'input.txt');
5
6function readBanks(filePath) {
7 try {
8 return fs
9 .readFileSync(filePath, 'utf8')
10 .trim()
11 .split(/\r?\n/)
12 .map((line) => line.trim())
13 .filter((line) => line.length > 0);
14 } catch (error) {
15 throw new Error(`Unable to read input file: ${filePath}`);
16 }
17}
18
19function maxTwoDigitValue(bank) {
20 if (bank.length < 2) {
21 return 0;
22 }
23
24 let bestPair = -1;
25 let bestFirst = Number(bank[0]);
26
27 for (let i = 1; i < bank.length; i += 1) {
28 const digit = Number(bank[i]);
29 const candidate = bestFirst * 10 + digit;
30 if (candidate > bestPair) {
31 bestPair = candidate;
32 }
33 if (digit > bestFirst) {
34 bestFirst = digit;
35 }
36 }
37
38 return bestPair;
39}
40
41function solve(banks) {
42 return banks.reduce((sum, bank) => sum + maxTwoDigitValue(bank), 0);
43}
44
45function main() {
46 if (!fs.existsSync(inputPath)) {
47 console.error(`Input file not found: ${inputPath}`);
48 process.exitCode = 1;
49 return;
50 }
51
52 const banks = readBanks(inputPath);
53 if (banks.length === 0) {
54 console.log('0');
55 return;
56 }
57
58 const result = solve(banks);
59 console.log(result.toString());
60}
61
62main();
Part 2 – Twelve digits to rule the escalator
The safety override demanded far more juice: exactly twelve digits per bank. This morphs into the classic “remove k digits to maximize the remaining sequence” problem. I used a monotonic stack:
k = bank.length - 12is how many digits must be discarded.- While the stack’s last digit is smaller than the current digit and I still have removals left, pop it.
- Push the current digit.
- After processing the whole bank, trim any extra digits from the end until only twelve remain.
Converting the resulting 12-digit string to BigInt kept the totals precise, and summing across all banks produced the final answer.
Complexity: The same total input length L drives this part. Each digit is pushed and popped at most once, so the runtime is O(L) with O(12) extra storage per bank (effectively O(1)).
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();
Lessons learned
- Greedy strategies shine when the problem boils down to keeping digits in order but maximizing lexicographic value.
- The same parsing foundation (reading banks from
input.txt) can feed both parts with different solving strategies. BigIntis a great safety net when puzzle outputs can exceed 64-bit integers.
With both parts done, the escalator has all the joltage it needs—and I get to keep moving deeper into the North Pole base.